require "test_helper"

class Security::ProvidedTest < ActiveSupport::TestCase
  include ProviderTestHelper

  setup do
    @security = securities(:aapl)
  end

  # --- search_provider ---

  test "search_provider returns results from multiple providers" do
    provider_a = mock("provider_a")
    provider_b = mock("provider_b")

    result_a = Provider::SecurityConcept::Security.new(
      symbol: "AAPL", name: "Apple Inc", logo_url: nil,
      exchange_operating_mic: "XNAS", country_code: "US", currency: "USD"
    )
    result_b = Provider::SecurityConcept::Security.new(
      symbol: "AAPL", name: "Apple Inc", logo_url: nil,
      exchange_operating_mic: "XNAS", country_code: "US", currency: "USD"
    )

    provider_a.stubs(:class).returns(Provider::TwelveData)
    provider_b.stubs(:class).returns(Provider::YahooFinance)

    provider_a.expects(:search_securities).with("AAPL").returns(
      provider_success_response([ result_a ])
    )
    provider_b.expects(:search_securities).with("AAPL").returns(
      provider_success_response([ result_b ])
    )

    Security.stubs(:providers).returns([ provider_a, provider_b ])

    results = Security.search_provider("AAPL")

    # Same ticker+exchange but different providers → both appear (dedup includes provider key)
    assert_equal 2, results.size
    assert results.all? { |s| s.ticker == "AAPL" }
    providers_in_results = results.map(&:price_provider).sort
    assert_includes providers_in_results, "twelve_data"
    assert_includes providers_in_results, "yahoo_finance"
  end

  test "search_provider deduplicates same ticker+exchange+provider" do
    provider = mock("provider")
    provider.stubs(:class).returns(Provider::TwelveData)

    dup_result = Provider::SecurityConcept::Security.new(
      symbol: "MSFT", name: "Microsoft", logo_url: nil,
      exchange_operating_mic: "XNAS", country_code: "US", currency: "USD"
    )

    provider.expects(:search_securities).with("MSFT").returns(
      provider_success_response([ dup_result, dup_result ])
    )

    Security.stubs(:providers).returns([ provider ])

    results = Security.search_provider("MSFT")
    assert_equal 1, results.size
  end

  test "search_provider returns empty array for blank symbol" do
    assert_equal [], Security.search_provider("")
    assert_equal [], Security.search_provider(nil)
  end

  test "search_provider returns empty array when no providers configured" do
    Security.stubs(:providers).returns([])
    assert_equal [], Security.search_provider("AAPL")
  end

  test "search_provider keeps fast provider results when slow provider times out" do
    fast_provider = mock("fast_provider")
    slow_provider = mock("slow_provider")

    fast_provider.stubs(:class).returns(Provider::TwelveData)
    slow_provider.stubs(:class).returns(Provider::YahooFinance)

    fast_result = Provider::SecurityConcept::Security.new(
      symbol: "SPY", name: "SPDR S&P 500", logo_url: nil,
      exchange_operating_mic: "XNAS", country_code: "US", currency: "USD"
    )

    fast_provider.expects(:search_securities).with("SPY").returns(
      provider_success_response([ fast_result ])
    )
    slow_provider.expects(:search_securities).with("SPY").returns(
      provider_success_response([])
    )

    Security.stubs(:providers).returns([ fast_provider, slow_provider ])

    results = Security.search_provider("SPY")

    assert results.size >= 1, "Fast provider results should be returned even if slow provider returns nothing"
    assert_equal "SPY", results.first.ticker
  end

  test "search_provider handles provider error gracefully" do
    good_provider = mock("good_provider")
    bad_provider = mock("bad_provider")

    good_provider.stubs(:class).returns(Provider::TwelveData)
    bad_provider.stubs(:class).returns(Provider::YahooFinance)

    good_result = Provider::SecurityConcept::Security.new(
      symbol: "GOOG", name: "Alphabet", logo_url: nil,
      exchange_operating_mic: "XNAS", country_code: "US", currency: "USD"
    )

    good_provider.expects(:search_securities).with("GOOG").returns(
      provider_success_response([ good_result ])
    )
    bad_provider.expects(:search_securities).with("GOOG").raises(StandardError, "API down")

    Security.stubs(:providers).returns([ good_provider, bad_provider ])

    results = Security.search_provider("GOOG")

    assert_equal 1, results.size
    assert_equal "GOOG", results.first.ticker
  end

  # --- price_data_provider ---

  test "price_data_provider returns assigned provider" do
    provider = mock("tiingo_provider")
    Security.stubs(:provider_for).with("tiingo").returns(provider)

    @security.update!(price_provider: "tiingo")

    assert_equal provider, @security.price_data_provider
  end

  test "price_data_provider returns nil when assigned provider is disabled" do
    Security.stubs(:provider_for).with("tiingo").returns(nil)

    @security.update!(price_provider: "tiingo")

    assert_nil @security.price_data_provider
  end

  test "price_data_provider falls back to first provider when none assigned" do
    fallback_provider = mock("fallback")
    Security.stubs(:providers).returns([ fallback_provider ])

    @security.update!(price_provider: nil)

    assert_equal fallback_provider, @security.price_data_provider
  end

  # --- provider_status ---

  test "provider_status returns provider_unavailable when assigned provider disabled" do
    Security.stubs(:provider_for).with("tiingo").returns(nil)

    @security.update!(price_provider: "tiingo")

    assert_equal :provider_unavailable, @security.provider_status
  end

  test "provider_status returns ok for healthy security" do
    provider = mock("provider")
    Security.stubs(:provider_for).with("twelve_data").returns(provider)

    @security.update!(price_provider: "twelve_data", offline: false, failed_fetch_count: 0)

    assert_equal :ok, @security.provider_status
  end

  # --- rank_search_results ---

  # Helper to build unsaved Security objects for ranking tests
  def build_result(ticker:, name: nil, country_code: nil, exchange_operating_mic: nil)
    Security.new(
      ticker: ticker,
      name: name || ticker,
      country_code: country_code,
      exchange_operating_mic: exchange_operating_mic
    )
  end

  def rank(results, query, country_code = nil)
    Security.send(:rank_search_results, results, query, country_code)
  end

  test "ranking: AAPL exact match ranks above AAPL-prefixed and unrelated" do
    results = [
      build_result(ticker: "AAPLX", name: "Some AAPL Fund"),
      build_result(ticker: "AAPL", name: "Apple Inc", country_code: "US", exchange_operating_mic: "XNAS"),
      build_result(ticker: "AAPL", name: "Apple Inc", country_code: "GB", exchange_operating_mic: "XLON"),
      build_result(ticker: "AAPLD", name: "AAPL Dividend ETF")
    ]

    ranked = rank(results, "AAPL", "US")

    # Exact matches first, US preferred over GB
    assert_equal "AAPL", ranked[0].ticker
    assert_equal "US", ranked[0].country_code
    assert_equal "AAPL", ranked[1].ticker
    assert_equal "GB", ranked[1].country_code
    # Prefix matches after
    assert ranked[2..].all? { |s| s.ticker.start_with?("AAPL") && s.ticker != "AAPL" }
  end

  test "ranking: Apple name search surfaces Apple Inc above unrelated" do
    results = [
      build_result(ticker: "PINEAPPLE", name: "Pineapple Corp"),
      build_result(ticker: "AAPL", name: "Apple Inc", country_code: "US"),
      build_result(ticker: "APLE", name: "Apple Hospitality REIT"),
      build_result(ticker: "APPL", name: "Appell Petroleum")
    ]

    ranked = rank(results, "Apple", "US")

    # No ticker matches "APPLE", so all fall to name-contains or worse.
    # "Apple Inc" and "Apple Hospitality" and "Pineapple" contain "APPLE" in name.
    # "Appell Petroleum" does not contain "APPLE".
    # Among name matches, alphabetical ticker breaks ties.
    name_matches = ranked.select { |s| s.name.upcase.include?("APPLE") }
    non_matches = ranked.reject { |s| s.name.upcase.include?("APPLE") }
    assert name_matches.size >= 2
    assert_equal non_matches, ranked.last(non_matches.size)
  end

  test "ranking: SPX exact match first, then SPX-prefixed" do
    results = [
      build_result(ticker: "SPXL", name: "Direxion Daily S&P 500 Bull 3X"),
      build_result(ticker: "SPXS", name: "Direxion Daily S&P 500 Bear 3X"),
      build_result(ticker: "SPX", name: "S&P 500 Index", country_code: "US"),
      build_result(ticker: "SPXU", name: "ProShares UltraPro Short S&P 500")
    ]

    ranked = rank(results, "SPX", "US")

    assert_equal "SPX", ranked[0].ticker, "Exact match should be first"
    assert ranked[1..].all? { |s| s.ticker.start_with?("SPX") }
  end

  test "ranking: VTTI exact match first regardless of country" do
    results = [
      build_result(ticker: "VTI", name: "Vanguard Total Stock Market ETF", country_code: "US"),
      build_result(ticker: "VTTI", name: "VTTI Energy Partners", country_code: "US"),
      build_result(ticker: "VTTIX", name: "Vanguard Target 2060 Fund")
    ]

    ranked = rank(results, "VTTI", "US")

    assert_equal "VTTI", ranked[0].ticker, "Exact match should be first"
    assert_equal "VTTIX", ranked[1].ticker, "Prefix match second"
    assert_equal "VTI", ranked[2].ticker, "Non-matching ticker last"
  end

  test "ranking: iShares S&P multi-word query is contiguous substring match" do
    results = [
      build_result(ticker: "IVV", name: "iShares S&P 500 ETF", country_code: "US"),
      build_result(ticker: "CSPX", name: "iShares Core S&P 500 UCITS ETF", country_code: "GB"),
      build_result(ticker: "IJH", name: "iShares S&P Mid-Cap ETF", country_code: "US"),
      build_result(ticker: "UNRELATED", name: "Something Else Corp")
    ]

    ranked = rank(results, "iShares S&P", "US")

    # Only names containing the exact substring "iShares S&P" match tier 2.
    # "iShares Core S&P" does NOT match (word "Core" breaks contiguity).
    contiguous_matches = ranked.select { |s| s.name.upcase.include?("ISHARES S&P") }
    assert_equal 2, contiguous_matches.size, "Only IVV and IJH contain the exact substring"
    # US contiguous matches should come first
    assert_equal "IJH", ranked[0].ticker  # US, name match, alphabetically before IVV? No...
    assert_includes [ "IVV", "IJH" ], ranked[0].ticker
    assert_includes [ "IVV", "IJH" ], ranked[1].ticker
    # Non-contiguous and unrelated should be last
    assert_equal "UNRELATED", ranked.last.ticker
  end

  test "ranking: tesla name search finds TSLA" do
    results = [
      build_result(ticker: "TSLA", name: "Tesla Inc", country_code: "US"),
      build_result(ticker: "TSLA", name: "Tesla Inc", country_code: "DE"),
      build_result(ticker: "TL0", name: "Tesla Inc", country_code: "DE", exchange_operating_mic: "XETR"),
      build_result(ticker: "TELSA", name: "Telsa Mining Ltd")
    ]

    ranked = rank(results, "tesla", "US")

    # No ticker matches "TESLA", so all go to name matching
    # "Tesla Inc" contains "TESLA" → tier 2, US preferred
    assert_equal "TSLA", ranked[0].ticker
    assert_equal "US", ranked[0].country_code, "US Tesla should rank first for US user"
  end
end
