require "test_helper"

class FetchIpapiDataJobTest < ActiveJob::TestCase
  setup do
    @tracking_network = NetworkRange.create!(
      network: "192.168.1.0/24",
      source: "auto_generated",
      creation_reason: "IPAPI tracking network"
    )
    @sample_ipapi_data = {
      "ip" => "192.168.1.100",
      "type" => "ipv4",
      "continent_code" => "NA",
      "continent_name" => "North America",
      "country_code" => "US",
      "country_name" => "United States",
      "region_code" => "CA",
      "region_name" => "California",
      "city" => "San Francisco",
      "zip" => "94102",
      "latitude" => 37.7749,
      "longitude" => -122.4194,
      "location" => {
        "geoname_id" => 5391959,
        "capital" => "Washington D.C.",
        "languages" => [
          {
            "code" => "en",
            "name" => "English",
            "native" => "English"
          }
        ],
        "country_flag" => "https://cdn.ipapi.com/flags/us.svg",
        "country_flag_emoji" => "🇺🇸",
        "country_flag_emoji_unicode" => "U+1F1FA U+1F1F8",
        "calling_code" => "1",
        "is_eu" => false
      },
      "time_zone" => {
        "id" => "America/Los_Angeles",
        "current_time" => "2023-12-07T12:00:00+00:00",
        "gmt_offset" => -28800,
        "code" => "PST",
        "is_dst" => false
      },
      "currency" => {
        "code" => "USD",
        "name" => "US Dollar",
        "plural" => "US dollars",
        "symbol" => "$",
        "symbol_native" => "$"
      },
      "connection" => {
        "asn" => 12345,
        "isp" => "Test ISP",
        "domain" => "test.com",
        "type" => "isp"
      },
      "security" => {
        "is_proxy" => false,
        "is_crawler" => false,
        "is_tor" => false,
        "threat_level" => "low",
        "threat_types" => []
      },
      "asn" => {
        "asn" => "AS12345 Test ISP",
        "domain" => "test.com",
        "route" => "192.168.1.0/24",
        "type" => "isp"
      }
    }
  end

  teardown do
    # Clean up any test networks
    NetworkRange.where(network: "192.168.1.0/24").delete_all
    NetworkRange.where(network: "203.0.113.0/24").delete_all
  end

  # Successful Data Fetching
  test "fetches and stores IPAPI data successfully" do
    # Mock Ipapi.lookup
    Ipapi.expects(:lookup).with("192.168.1.0").returns(@sample_ipapi_data)

    FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)

    @tracking_network.reload
    assert_equal @sample_ipapi_data, @tracking_network.network_data_for(:ipapi)
    assert_not_nil @tracking_network.last_api_fetch
    assert @tracking_network.network_data['ipapi_queried_at'] > 5.seconds.ago.to_i
    assert_equal "192.168.1.0/24", @tracking_network.network_data['ipapi_returned_cidr']
  end

  test "handles IPAPI returning different route than tracking network" do
    # IPAPI returns a more specific network
    different_route_data = @sample_ipapi_data.dup
    different_route_data["asn"]["route"] = "203.0.113.0/25"

    Ipapi.expects(:lookup).with("192.168.1.0").returns(different_route_data)

    FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)

    # Should create new network range for the correct route
    target_network = NetworkRange.find_by(network: "203.0.113.0/25")
    assert_not_nil target_network
    assert_equal different_route_data, target_network.network_data_for(:ipapi)
    assert_equal "api_imported", target_network.source
    assert_match /Created from IPAPI lookup/, target_network.creation_reason

    # Tracking network should be marked as queried with the returned CIDR
    @tracking_network.reload
    assert_equal "203.0.113.0/25", @tracking_network.network_data['ipapi_returned_cidr']
  end

  test "uses existing network when IPAPI returns different route" do
    # Create the target network first
    existing_network = NetworkRange.create!(
      network: "203.0.113.0/25",
      source: "manual",
      creation_reason: "Pre-existing"
    )

    different_route_data = @sample_ipapi_data.dup
    different_route_data["asn"]["route"] = "203.0.113.0/25"

    Ipapi.expects(:lookup).with("192.168.1.0").returns(different_route_data)

    FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)

    # Should use existing network, not create new one
    existing_network.reload
    assert_equal different_route_data, existing_network.network_data_for(:ipapi)
    assert_equal 1, NetworkRange.where(network: "203.0.113.0/25").count
  end

  # Error Handling
  test "handles IPAPI returning error gracefully" do
    error_data = {
      "error" => true,
      "reason" => "Invalid IP address",
      "ip" => "192.168.1.0"
    }

    Ipapi.expects(:lookup).with("192.168.1.0").returns(error_data)

    FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)

    # Should mark as queried to avoid immediate retry
    @tracking_network.reload
    assert @tracking_network.network_data['ipapi_queried_at'] > 5.seconds.ago.to_i
    assert_equal "192.168.1.0/24", @tracking_network.network_data['ipapi_returned_cidr']

    # Should not store the error data
    assert_empty @tracking_network.network_data_for(:ipapi)
  end

  test "handles IPAPI returning nil gracefully" do
    Ipapi.expects(:lookup).with("192.168.1.0").returns(nil)

    FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)

    # Should mark as queried to avoid immediate retry
    @tracking_network.reload
    assert @tracking_network.network_data['ipapi_queried_at'] > 5.seconds.ago.to_i
    assert_equal "192.168.1.0/24", @tracking_network.network_data['ipapi_returned_cidr']
  end

  test "handles missing network range gracefully" do
    # Use non-existent network range ID
    assert_nothing_raised do
      FetchIpapiDataJob.perform_now(network_range_id: 99999)
    end
  end

  test "handles IPAPI service errors gracefully" do
    Ipapi.expects(:lookup).with("192.168.1.0").raises(StandardError.new("Service unavailable"))

    # Should not raise error but should clear fetching status
    assert_nothing_raised do
      FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)
    end

    # Fetching status should be cleared
    assert_not @tracking_network.is_fetching_api_data?(:ipapi)
  end

  # Fetching Status Management
  test "clears fetching status when done" do
    @tracking_network.mark_as_fetching_api_data!(:ipapi)

    Ipapi.expects(:lookup).with("192.168.1.0").returns(@sample_ipapi_data)

    assert @tracking_network.is_fetching_api_data?(:ipapi)
    FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)
    assert_not @tracking_network.is_fetching_api_data?(:ipapi)
  end

  test "clears fetching status even on error" do
    @tracking_network.mark_as_fetching_api_data!(:ipapi)

    Ipapi.expects(:lookup).with("192.168.1.0").raises(StandardError.new("Service error"))

    assert @tracking_network.is_fetching_api_data?(:ipapi)
    FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)
    assert_not @tracking_network.is_fetching_api_data?(:ipapi)
  end

  test "clears fetching status when network range not found" do
    # Create network range and mark as fetching
    temp_network = NetworkRange.create!(
      network: "10.0.0.0/24",
      source: "auto_generated"
    )
    temp_network.mark_as_fetching_api_data!(:ipapi)

    # Try to fetch with non-existent ID
    FetchIpapiDataJob.perform_now(network_range_id: 99999)

    # Original network should still have fetching status cleared (ensure block runs)
    temp_network.reload
    assert_not temp_network.is_fetching_api_data?(:ipapi)
  end

  # Turbo Broadcast
  test "broadcasts IPAPI update on success" do
    Ipapi.expects(:lookup).with("192.168.1.0").returns(@sample_ipapi_data)

    # Expect Turbo broadcast
    Turbo::StreamsChannel.expects(:broadcast_replace_to)
      .with("network_range_#{@tracking_network.id}", {
        target: "ipapi_data_section",
        partial: "network_ranges/ipapi_data",
        locals: {
          ipapi_data: @sample_ipapi_data,
          network_range: @tracking_network,
          parent_with_ipapi: nil,
          ipapi_loading: false
        }
      })

    FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)
  end

  test "does not broadcast on error" do
    error_data = { "error" => true, "reason" => "Invalid IP" }
    Ipapi.expects(:lookup).with("192.168.1.0").returns(error_data)

    # Should not broadcast
    Turbo::StreamsChannel.expects(:broadcast_replace_to).never

    FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)
  end

  # Network Address Extraction
  test "extracts correct sample IP from network" do
    # Test with different network formats
    ipv4_network = NetworkRange.create!(network: "203.0.113.0/24")
    Ipapi.expects(:lookup).with("203.0.113.0").returns(@sample_ipapi_data)

    FetchIpapiDataJob.perform_now(network_range_id: ipv4_network.id)

    ipv6_network = NetworkRange.create!(network: "2001:db8::/64")
    Ipapi.expects(:lookup).with("2001:db8::").returns(@sample_ipapi_data)

    FetchIpapiDataJob.perform_now(network_range_id: ipv6_network.id)
  end

  # Data Storage
  test "stores complete IPAPI data in network_data" do
    Ipapi.expects(:lookup).with("192.168.1.0").returns(@sample_ipapi_data)

    FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)

    stored_data = @tracking_network.reload.network_data_for(:ipapi)
    assert_equal @sample_ipapi_data["country_code"], stored_data["country_code"]
    assert_equal @sample_ipapi_data["city"], stored_data["city"]
    assert_equal @sample_ipapi_data["asn"]["asn"], stored_data["asn"]["asn"]
    assert_equal @sample_ipapi_data["security"]["is_proxy"], stored_data["security"]["is_proxy"]
  end

  test "updates last_api_fetch timestamp" do
    original_time = 1.hour.ago
    @tracking_network.update!(last_api_fetch: original_time)

    Ipapi.expects(:lookup).with("192.168.1.0").returns(@sample_ipapi_data)

    FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)

    @tracking_network.reload
    assert @tracking_network.last_api_fetch > original_time
  end

  # IPv6 Support
  test "handles IPv6 networks correctly" do
    ipv6_network = NetworkRange.create!(
      network: "2001:db8::/64",
      source: "auto_generated",
      creation_reason: "IPAPI tracking network"
    )

    ipv6_data = @sample_ipapi_data.dup
    ipv6_data["ip"] = "2001:db8::1"
    ipv6_data["type"] = "ipv6"
    ipv6_data["asn"]["route"] = "2001:db8::/32"

    Ipapi.expects(:lookup).with("2001:db8::").returns(ipv6_data)

    FetchIpapiDataJob.perform_now(network_range_id: ipv6_network.id)

    ipv6_network.reload
    assert_equal ipv6_data, ipv6_network.network_data_for(:ipapi)
    assert_equal "2001:db8::/32", ipv6_network.network_data['ipapi_returned_cidr']
  end

  # Logging
  test "logs successful fetch" do
    log_output = StringIO.new
    logger = Logger.new(log_output)
    original_logger = Rails.logger
    Rails.logger = logger

    Ipapi.expects(:lookup).with("192.168.1.0").returns(@sample_ipapi_data)

    FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)

    log_content = log_output.string
    assert_match /Fetching IPAPI data for 192\.168\.1\.0\/24 using IP 192\.168\.1\.0/, log_content
    assert_match /Successfully fetched IPAPI data/, log_content

    Rails.logger = original_logger
  end

  test "logs errors and warnings" do
    log_output = StringIO.new
    logger = Logger.new(log_output)
    original_logger = Rails.logger
    Rails.logger = logger

    error_data = { "error" => true, "reason" => "Rate limited" }
    Ipapi.expects(:lookup).with("192.168.1.0").returns(error_data)

    FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)

    log_content = log_output.string
    assert_match /IPAPI returned error for 192\.168\.1\.0\/24/, log_content

    Rails.logger = original_logger
  end

  test "logs different route handling" do
    log_output = StringIO.new
    logger = Logger.new(log_output)
    original_logger = Rails.logger
    Rails.logger = logger

    different_route_data = @sample_ipapi_data.dup
    different_route_data["asn"]["route"] = "203.0.113.0/25"

    Ipapi.expects(:lookup).with("192.168.1.0").returns(different_route_data)

    FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)

    log_content = log_output.string
    assert_match /IPAPI returned different route: 203\.0\.113\.0\/25/, log_content
    assert_match /Storing IPAPI data on correct network: 203\.0\.113\.0\/25/, log_content

    Rails.logger = original_logger
  end

  test "logs service errors with backtrace" do
    log_output = StringIO.new
    logger = Logger.new(log_output)
    original_logger = Rails.logger
    Rails.logger = logger

    Ipapi.expects(:lookup).with("192.168.1.0").raises(StandardError.new("Connection failed"))

    FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)

    log_content = log_output.string
    assert_match /Failed to fetch IPAPI data for network_range #{@tracking_network.id}/, log_content
    assert_match /Connection failed/, log_content

    Rails.logger = original_logger
  end
end
