require "test_helper"

class Provider::YahooFinanceTest < ActiveSupport::TestCase
  setup do
    @provider = Provider::YahooFinance.new
  end

  # ================================
  #        Health Check Tests
  # ================================

  test "healthy? returns true when API is working" do
    mock_response = mock
    mock_response.stubs(:body).returns('{"chart":{"result":[{"meta":{"symbol":"AAPL"}}]}}')

    @provider.stubs(:fetch_cookie_and_crumb).returns([ "test_cookie", "test_crumb" ])
    @provider.stubs(:authenticated_client).returns(mock_client = mock)
    mock_client.stubs(:get).returns(mock_response)

    assert @provider.healthy?
  end

  test "healthy? returns false when API fails" do
    @provider.stubs(:fetch_cookie_and_crumb).raises(Provider::YahooFinance::AuthenticationError.new("auth failed"))

    assert_not @provider.healthy?
  end

  test "healthy? retries with fresh crumb on Unauthorized body response" do
    unauthorized_body = '{"chart":{"error":{"code":"Unauthorized","description":"No crumb"}}}'
    success_body = '{"chart":{"result":[{"meta":{"symbol":"AAPL"}}]}}'

    unauthorized_response = mock
    unauthorized_response.stubs(:body).returns(unauthorized_body)

    success_response = mock
    success_response.stubs(:body).returns(success_body)

    mock_client = mock
    mock_client.stubs(:get).returns(unauthorized_response, success_response)

    @provider.stubs(:fetch_cookie_and_crumb).returns([ "cookie1", "crumb1" ], [ "cookie2", "crumb2" ])
    @provider.stubs(:authenticated_client).returns(mock_client)
    @provider.expects(:clear_crumb_cache).once

    assert @provider.healthy?
  end

  # ================================
  #      Exchange Rate Tests
  # ================================

  test "fetch_exchange_rate returns 1.0 for same currency" do
    date = Date.parse("2024-01-15")
    response = @provider.fetch_exchange_rate(from: "USD", to: "USD", date: date)

    assert response.success?
    rate = response.data
    assert_equal 1.0, rate.rate
    assert_equal "USD", rate.from
    assert_equal "USD", rate.to
    assert_equal date, rate.date
  end

  test "fetch_exchange_rate handles invalid currency codes" do
    date = Date.parse("2024-01-15")

    # With validation removed, invalid currencies will result in API errors
    response = @provider.fetch_exchange_rate(from: "INVALID", to: "USD", date: date)
    assert_not response.success?
    assert_instance_of Provider::YahooFinance::Error, response.error

    response = @provider.fetch_exchange_rate(from: "USD", to: "INVALID", date: date)
    assert_not response.success?
    assert_instance_of Provider::YahooFinance::Error, response.error

    response = @provider.fetch_exchange_rate(from: "", to: "USD", date: date)
    assert_not response.success?
    assert_instance_of Provider::YahooFinance::Error, response.error
  end

  test "fetch_exchange_rates returns same currency rates" do
    start_date = Date.parse("2024-01-10")
    end_date = Date.parse("2024-01-12")
    response = @provider.fetch_exchange_rates(from: "USD", to: "USD", start_date: start_date, end_date: end_date)

    assert response.success?
    rates = response.data
    expected_dates = (start_date..end_date).to_a
    assert_equal expected_dates.length, rates.length
    assert rates.all? { |r| r.rate == 1.0 }
    assert rates.all? { |r| r.from == "USD" }
    assert rates.all? { |r| r.to == "USD" }
  end

  test "fetch_exchange_rates validates date range" do
    response = @provider.fetch_exchange_rates(from: "USD", to: "EUR", start_date: Date.current, end_date: Date.current - 1.day)
    assert_not response.success?
    assert_instance_of Provider::YahooFinance::Error, response.error

    response = @provider.fetch_exchange_rates(from: "USD", to: "EUR", start_date: Date.current - 6.years, end_date: Date.current)
    assert_not response.success?
    assert_instance_of Provider::YahooFinance::Error, response.error
  end

  # ================================
  #       Security Search Tests
  # ================================

  test "search_securities handles invalid symbols" do
    # With validation removed, invalid symbols will result in API errors
    response = @provider.search_securities("")
    assert_not response.success?
    assert_instance_of Provider::YahooFinance::Error, response.error

    response = @provider.search_securities("VERYLONGSYMBOLNAME")
    assert_not response.success?
    assert_instance_of Provider::YahooFinance::Error, response.error

    response = @provider.search_securities("INVALID@SYMBOL")
    assert_not response.success?
    assert_instance_of Provider::YahooFinance::Error, response.error
  end

  test "search_securities returns empty array for no results with short symbol" do
    # Mock empty results response
    mock_response = mock
    mock_response.stubs(:body).returns('{"quotes":[]}')

    @provider.stubs(:client).returns(mock_client = mock)
    mock_client.stubs(:get).returns(mock_response)

    response = @provider.search_securities("XYZ")
    assert response.success?
    assert_equal [], response.data
  end

  # ================================
  #     Security Price Tests
  # ================================

  test "fetch_security_price handles invalid symbol" do
    date = Date.parse("2024-01-15")

    # With validation removed, invalid symbols will result in API errors
    response = @provider.fetch_security_price(symbol: "", exchange_operating_mic: "XNAS", date: date)
    assert_not response.success?
    assert_instance_of Provider::YahooFinance::Error, response.error
  end

  # ================================
  #         Caching Tests
  # ================================

  # Note: Caching tests are skipped as Rails.cache may not be properly configured in test environment
  # and caching functionality is not the focus of the validation fixes

  # ================================
  #       Error Handling Tests
  # ================================

  test "handles Faraday errors gracefully" do
    # Mock a Faraday error
    faraday_error = Faraday::ConnectionFailed.new("Connection failed")

    @provider.stub :client, ->(*) { raise faraday_error } do
      result = @provider.send(:with_provider_response) { raise faraday_error }

      assert_not result.success?
      assert_instance_of Provider::YahooFinance::Error, result.error
    end
  end

  test "handles rate limit errors" do
    rate_limit_error = Faraday::TooManyRequestsError.new("Rate limit exceeded", { body: "Too many requests" })

    @provider.stub :client, ->(*) { raise rate_limit_error } do
      result = @provider.send(:with_provider_response) { raise rate_limit_error }

      assert_not result.success?
      assert_instance_of Provider::YahooFinance::RateLimitError, result.error
    end
  end

  test "handles 401 unauthorized as authentication error" do
    unauthorized_error = Faraday::UnauthorizedError.new("Unauthorized", { body: "Invalid Crumb" })

    @provider.stub :client, ->(*) { raise unauthorized_error } do
      result = @provider.send(:with_provider_response) { raise unauthorized_error }

      assert_not result.success?
      assert_instance_of Provider::YahooFinance::AuthenticationError, result.error
      assert_match(/authentication failed/, result.error.message)
    end
  end

  # ================================
  #     User-Agent Rotation Tests
  # ================================

  test "random_user_agent returns value from USER_AGENTS pool" do
    user_agent = @provider.send(:random_user_agent)
    assert_includes Provider::YahooFinance::USER_AGENTS, user_agent
  end

  test "USER_AGENTS contains multiple modern browser user-agents" do
    assert Provider::YahooFinance::USER_AGENTS.length >= 5
    assert Provider::YahooFinance::USER_AGENTS.all? { |ua| ua.include?("Mozilla") }
  end

  # ================================
  #       Throttling Tests
  # ================================

  test "throttle_request enforces minimum interval between requests" do
    # First request should not wait
    start_time = Time.current
    @provider.send(:throttle_request)
    first_elapsed = Time.current - start_time
    assert first_elapsed < 0.1, "First request should not wait"

    # Second request should wait approximately min_request_interval
    start_time = Time.current
    @provider.send(:throttle_request)
    second_elapsed = Time.current - start_time
    min_interval = @provider.send(:min_request_interval)
    assert second_elapsed >= (min_interval - 0.05), "Second request should wait at least #{min_interval - 0.05}s"
  end

  # ================================
  #    Configuration Tests
  # ================================

  test "max_retries returns default value" do
    assert_equal 5, @provider.send(:max_retries)
  end

  test "retry_interval returns default value" do
    assert_equal 1.0, @provider.send(:retry_interval)
  end

  test "min_request_interval returns default value" do
    assert_equal 0.5, @provider.send(:min_request_interval)
  end

  # ================================
  #  Cookie/Crumb Authentication Tests
  # ================================

  test "extract_cookie extracts cookie from set-cookie header" do
    mock_response = OpenStruct.new(
      headers: { "set-cookie" => "B=abc123&b=3&s=qf; expires=Fri, 18-May-2028 00:00:00 GMT; path=/; domain=.yahoo.com" }
    )

    cookie = @provider.send(:extract_cookie, mock_response)
    assert_equal "B=abc123&b=3&s=qf", cookie
  end

  test "extract_cookie returns nil when no cookie header" do
    mock_response = OpenStruct.new(headers: {})
    cookie = @provider.send(:extract_cookie, mock_response)
    assert_nil cookie
  end

  test "extract_cookie_max_age parses Max-Age from cookie header" do
    mock_response = OpenStruct.new(
      headers: { "set-cookie" => "A3=d=xxx; Max-Age=31557600; Domain=.yahoo.com" }
    )

    max_age = @provider.send(:extract_cookie_max_age, mock_response)
    assert_equal 31557600.seconds, max_age
  end

  test "extract_cookie_max_age returns nil when no Max-Age" do
    mock_response = OpenStruct.new(
      headers: { "set-cookie" => "A3=d=xxx; Domain=.yahoo.com" }
    )

    max_age = @provider.send(:extract_cookie_max_age, mock_response)
    assert_nil max_age
  end

  test "clear_crumb_cache removes cached crumb" do
    Rails.cache.write("yahoo_finance_auth_crumb", [ "cookie", "crumb" ])
    @provider.send(:clear_crumb_cache)
    assert_nil Rails.cache.read("yahoo_finance_auth_crumb")
  end

  # ================================
  #       Helper Method Tests
  # ================================

  test "map_country_code returns correct codes for exchanges" do
    assert_equal "US", @provider.send(:map_country_code, "NASDAQ")
    assert_equal "US", @provider.send(:map_country_code, "NYSE")
    assert_equal "GB", @provider.send(:map_country_code, "LSE")
    assert_equal "JP", @provider.send(:map_country_code, "TOKYO")
    assert_equal "CA", @provider.send(:map_country_code, "TSX")
    assert_equal "DE", @provider.send(:map_country_code, "FRANKFURT")
    assert_nil @provider.send(:map_country_code, "UNKNOWN")
    assert_nil @provider.send(:map_country_code, "")
  end

  test "map_exchange_mic returns correct MIC codes" do
    assert_equal "XNAS", @provider.send(:map_exchange_mic, "NMS")
    assert_equal "XNAS", @provider.send(:map_exchange_mic, "NGM")
    assert_equal "XNYS", @provider.send(:map_exchange_mic, "NYQ")
    assert_equal "XLON", @provider.send(:map_exchange_mic, "LSE")
    assert_equal "XTSE", @provider.send(:map_exchange_mic, "TSE")
    assert_equal "UNKNOWN", @provider.send(:map_exchange_mic, "UNKNOWN")
    assert_nil @provider.send(:map_exchange_mic, "")
  end

  test "map_security_type returns correct types" do
    assert_equal "common stock", @provider.send(:map_security_type, "equity")
    assert_equal "etf", @provider.send(:map_security_type, "etf")
    assert_equal "mutual fund", @provider.send(:map_security_type, "mutualfund")
    assert_equal "index", @provider.send(:map_security_type, "index")
    assert_equal "unknown", @provider.send(:map_security_type, "unknown")
    assert_nil @provider.send(:map_security_type, nil)
  end



  test "validate_date_range! raises errors for invalid ranges" do
    assert_raises(Provider::YahooFinance::Error) do
      @provider.send(:validate_date_range!, Date.current, Date.current - 1.day)
    end

    assert_raises(Provider::YahooFinance::Error) do
      @provider.send(:validate_date_range!, Date.current - 6.years - 1.day, Date.current)
    end

    # Should not raise for valid ranges
    assert_nothing_raised do
      @provider.send(:validate_date_range!, Date.current - 1.year, Date.current)
      @provider.send(:validate_date_range!, Date.current - 5.years, Date.current)
    end
  end

  # ================================
  #   Currency Normalization Tests
  # ================================

  test "normalize_currency_and_price converts GBp to GBP" do
    currency, price = @provider.send(:normalize_currency_and_price, "GBp", 1234.56)
    assert_equal "GBP", currency
    assert_equal 12.3456, price
  end

  test "normalize_currency_and_price converts ZAc to ZAR" do
    currency, price = @provider.send(:normalize_currency_and_price, "ZAc", 5000.0)
    assert_equal "ZAR", currency
    assert_equal 50.0, price
  end

  test "normalize_currency_and_price leaves standard currencies unchanged" do
    currency, price = @provider.send(:normalize_currency_and_price, "USD", 100.50)
    assert_equal "USD", currency
    assert_equal 100.50, price

    currency, price = @provider.send(:normalize_currency_and_price, "GBP", 50.25)
    assert_equal "GBP", currency
    assert_equal 50.25, price

    currency, price = @provider.send(:normalize_currency_and_price, "EUR", 75.75)
    assert_equal "EUR", currency
    assert_equal 75.75, price
  end

  # ================================
  #   Exchange Mapping Tests
  # ================================

  test "map_exchange_mic returns XNSE for NSE and NSI" do
    assert_equal "XNSE", @provider.send(:map_exchange_mic, "NSE")
    assert_equal "XNSE", @provider.send(:map_exchange_mic, "NSI")
    assert_equal "XNSE", @provider.send(:map_exchange_mic, "nse")
  end

  test "map_exchange_mic returns XBOM for BSE and BOM" do
    assert_equal "XBOM", @provider.send(:map_exchange_mic, "BSE")
    assert_equal "XBOM", @provider.send(:map_exchange_mic, "BOM")
  end

  test "map_country_code returns IN for Indian exchanges" do
    assert_equal "IN", @provider.send(:map_country_code, "NSE")
    assert_equal "IN", @provider.send(:map_country_code, "BSE")
    assert_equal "IN", @provider.send(:map_country_code, "MUMBAI")
  end

  # ================================
  #   normalize_symbol Tests
  # ================================

  test "normalize_symbol appends configured suffix for known MICs" do
    assert_equal "RELIANCE.NS", @provider.send(:normalize_symbol, "RELIANCE", "XNSE")
    assert_equal "INFY.NS",     @provider.send(:normalize_symbol, "INFY", "XNSE")
    assert_equal "500325.BO",   @provider.send(:normalize_symbol, "500325", "XBOM")
  end

  test "normalize_symbol does not double-suffix already suffixed symbols" do
    assert_equal "RELIANCE.NS", @provider.send(:normalize_symbol, "RELIANCE.NS", "XNSE")
    assert_equal "500325.BO",   @provider.send(:normalize_symbol, "500325.BO", "XBOM")
  end

  test "normalize_symbol leaves unconfigured MIC symbols unchanged" do
    assert_equal "AAPL", @provider.send(:normalize_symbol, "AAPL", "XNAS")
    assert_equal "BARC", @provider.send(:normalize_symbol, "BARC", "XLON")
    assert_equal "AAPL", @provider.send(:normalize_symbol, "AAPL", nil)
  end

  test "normalize_symbol appends suffix to dotted symbols that do not already end with the configured suffix" do
    assert_equal "BRK.A.NS", @provider.send(:normalize_symbol, "BRK.A", "XNSE")
    assert_equal "BRK.B.BO", @provider.send(:normalize_symbol, "BRK.B", "XBOM")
  end

  # ================================
  #  default_currency_for_exchange Tests
  # ================================

  test "default_currency_for_exchange returns configured currency for known Yahoo exchange names" do
    assert_equal "INR", @provider.send(:default_currency_for_exchange, "NSE")
    assert_equal "INR", @provider.send(:default_currency_for_exchange, "BSE")
  end

  test "default_currency_for_exchange returns nil for unknown exchanges" do
    assert_nil @provider.send(:default_currency_for_exchange, "UNKNOWN")
    assert_nil @provider.send(:default_currency_for_exchange, "NMS")
  end

  # ================================
  #  deduplicate_dual_listings Tests
  # ================================

  test "deduplicate_dual_listings keeps preferred exchange when both are present" do
    nse = Provider::SecurityConcept::Security.new(symbol: "RELIANCE.NS", name: "Reliance", logo_url: nil, exchange_operating_mic: "XNSE", country_code: "IN")
    bse = Provider::SecurityConcept::Security.new(symbol: "500325.BO",   name: "Reliance", logo_url: nil, exchange_operating_mic: "XBOM", country_code: "IN")
    other = Provider::SecurityConcept::Security.new(symbol: "OTHER", name: "Other", logo_url: nil, exchange_operating_mic: "XNAS", country_code: "US")

    result = @provider.send(:deduplicate_dual_listings, [ nse, bse, other ])

    assert_equal "XNSE", result.first.exchange_operating_mic
    assert_not result.map(&:exchange_operating_mic).include?("XBOM"), "Lower-ranked exchange should be removed"
    assert result.map(&:exchange_operating_mic).include?("XNAS"), "Non-dual-listed exchanges should be preserved"
  end

  test "deduplicate_dual_listings preserves unrelated securities in the same dual_list_group" do
    reliance_nse = Provider::SecurityConcept::Security.new(symbol: "RELIANCE.NS", name: "Reliance Industries", logo_url: nil, exchange_operating_mic: "XNSE", country_code: "IN")
    reliance_bse = Provider::SecurityConcept::Security.new(symbol: "500325.BO",   name: "Reliance Industries", logo_url: nil, exchange_operating_mic: "XBOM", country_code: "IN")
    infy_nse     = Provider::SecurityConcept::Security.new(symbol: "INFY.NS",     name: "Infosys",             logo_url: nil, exchange_operating_mic: "XNSE", country_code: "IN")
    other        = Provider::SecurityConcept::Security.new(symbol: "AAPL",        name: "Apple",               logo_url: nil, exchange_operating_mic: "XNAS", country_code: "US")

    result = @provider.send(:deduplicate_dual_listings, [ reliance_nse, reliance_bse, infy_nse, other ])

    symbols = result.map(&:symbol)
    assert_includes symbols, "RELIANCE.NS", "Preferred listing should be kept"
    assert_not_includes symbols, "500325.BO", "Duplicate listing should be removed"
    assert_includes symbols, "INFY.NS", "Unrelated security in same group should be preserved"
    assert_includes symbols, "AAPL", "Non-dual-listed security should be preserved"
    assert_equal 3, result.size
  end

  test "deduplicate_dual_listings returns original list when no dual-listed exchanges present" do
    securities = [
      Provider::SecurityConcept::Security.new(symbol: "AAPL", name: "Apple", logo_url: nil, exchange_operating_mic: "XNAS", country_code: "US"),
      Provider::SecurityConcept::Security.new(symbol: "MSFT", name: "Microsoft", logo_url: nil, exchange_operating_mic: "XNAS", country_code: "US")
    ]

    result = @provider.send(:deduplicate_dual_listings, securities)
    assert_equal securities, result
  end
end
