"""
Tests EODHDAdapter's request/response parsing against a mocked transport (httpx.MockTransport,
same pattern as test_fmp_adapter.py) -- no real network call is made. StockLab overhaul, final
engineering pass, Part A7: this file previously did not exist at all -- EODHD was the one real
provider adapter with zero test coverage.

AUDIT (Part A7): `httpx` is not installed in this sandbox (confirmed repeatedly throughout this
overhaul -- docs/TEST_REPORT.md), so this file -- like test_fmp_adapter.py before it -- could not
actually be EXECUTED here. It is verified only by `python3 -m py_compile` and careful manual
review against app/adapters/eodhd.py's real source. Do not read a passing claim into this file's
existence; see docs/AUDIT_EODHD_TESTS_A7.md for the honest TESTED/NOT TESTED status.

Coverage, per this pass's explicit shape list -- valid / missing-field / unexpected-field /
wrong-type / empty / API-error / partial-response -- applied to every EODHD method that has real
logic (get_company_profile, get_prices, list_universe, and the shared _get() error mapping).
get_income_statements/get_balance_sheets/get_cash_flows/get_estimates/get_dividends are all
unconditional NotImplementedError today (see eodhd.py's own module docstring) -- there is no field
mapping in them to exercise, so their tests only confirm the documented gap raises exactly what it
claims to, which matters in its own right (Part A6 found that an uncaught NotImplementedError from
exactly these methods would otherwise crash ingestion the moment PROVIDER_SECONDARY=EODHD is
configured -- see docs/AUDIT_PROVIDER_CONFLICT_A6.md §3).
"""
from __future__ import annotations

import httpx

from app.adapters.base import ProviderAuthError, ProviderNotFoundError, ProviderRateLimitError
from app.adapters.eodhd import EODHDAdapter

FAKE_PROFILE_RESPONSE = {
    "General": {
        "Name": "Apple Inc.", "Exchange": "NASDAQ", "CountryISO": "US", "Sector": "Technology",
        "Industry": "Consumer Electronics", "CurrencyCode": "USD", "ISIN": "US0378331005",
        "WebURL": "https://apple.com", "Description": "Makes phones.",
    },
}

FAKE_EOD_RESPONSE = [
    {"date": "2025-09-30", "open": 227.5, "high": 229.0, "low": 226.8, "close": 228.2,
     "adjusted_close": 228.2, "volume": 45000000},
]


def _mock_transport(handler):
    return httpx.MockTransport(handler)


# --- get_company_profile ---------------------------------------------------------------------

def test_get_company_profile_parses_known_fields_valid():
    def handler(request: httpx.Request) -> httpx.Response:
        assert request.url.path.endswith("/fundamentals/AAPL")
        assert "api_token" in request.url.params
        return httpx.Response(200, json=FAKE_PROFILE_RESPONSE)

    adapter = EODHDAdapter(api_key="fake", transport=_mock_transport(handler))
    profile = adapter.get_company_profile("AAPL")
    assert profile.legal_name == "Apple Inc."
    assert profile.country_iso2 == "US"
    assert profile.currency == "USD"
    assert profile.isin == "US0378331005"


def test_get_company_profile_missing_fields_defaults_gracefully():
    # "General" present but nearly empty -- every .get(..., default) path should be exercised,
    # not crash on a KeyError.
    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(200, json={"General": {}})

    adapter = EODHDAdapter(api_key="fake", transport=_mock_transport(handler))
    profile = adapter.get_company_profile("AAPL")
    assert profile.legal_name == "AAPL"  # falls back to the ticker, general.get("Name", ticker)
    assert profile.currency == "USD"     # falls back to "USD", general.get("CurrencyCode", "USD")
    assert profile.country_iso2 is None
    assert profile.sector is None
    assert profile.beta is None          # EODHDAdapter never populates beta -- always None


def test_get_company_profile_unexpected_extra_fields_ignored():
    # fields EODHD's real response is known to include that this adapter doesn't map at all
    # (e.g. "Officers", "Listings", a nested "Highlights" block) must not break parsing.
    response = {
        "General": {**FAKE_PROFILE_RESPONSE["General"], "UnmappedField": "some value", "Officers": {"0": {"Name": "Tim Cook"}}},
        "Highlights": {"MarketCapitalization": 3000000000000},
        "Technicals": {"Beta": 1.2},  # note: NOT read by get_company_profile -- beta stays None
    }

    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(200, json=response)

    adapter = EODHDAdapter(api_key="fake", transport=_mock_transport(handler))
    profile = adapter.get_company_profile("AAPL")
    assert profile.legal_name == "Apple Inc."
    assert profile.beta is None  # documents that EODHDAdapter does NOT read Technicals.Beta today


def test_get_company_profile_empty_response():
    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(200, json={})

    adapter = EODHDAdapter(api_key="fake", transport=_mock_transport(handler))
    profile = adapter.get_company_profile("AAPL")
    assert profile.legal_name == "AAPL"
    assert profile.currency == "USD"
    assert profile.country_iso2 is None
    assert profile.description is None


def test_get_company_profile_auth_error_401():
    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(401, text="unauthorized")

    adapter = EODHDAdapter(api_key="bad-key", transport=_mock_transport(handler))
    try:
        adapter.get_company_profile("AAPL")
        raised = False
    except ProviderAuthError:
        raised = True
    assert raised


def test_get_company_profile_rate_limit_429():
    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(429, text="rate limited")

    adapter = EODHDAdapter(api_key="fake", transport=_mock_transport(handler))
    try:
        adapter.get_company_profile("AAPL")
        raised = False
    except ProviderRateLimitError:
        raised = True
    assert raised


def test_get_company_profile_not_found_404():
    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(404, text="not found")

    adapter = EODHDAdapter(api_key="fake", transport=_mock_transport(handler))
    try:
        adapter.get_company_profile("NOPE")
        raised = False
    except ProviderNotFoundError:
        raised = True
    assert raised



def _prime_eodhd_price_currency(adapter, currency="EUR"):
    """Test helper: emulate production profile resolution before price ingestion."""
    adapter._profile_currency = currency
    assert adapter._profile_currency == currency


# --- get_prices --------------------------------------------------------------------------------

def test_get_prices_requires_profile_currency():
    from datetime import date as date_cls
    from app.adapters.base import ProviderNotFoundError

    def handler(request: httpx.Request) -> httpx.Response:
        raise AssertionError("HTTP must not be called without provider currency")

    adapter = EODHDAdapter(
        api_key="fake",
        transport=_mock_transport(handler),
    )

    try:
        adapter.get_prices(
            "AAPL",
            date_cls(2025, 9, 1),
            date_cls(2025, 9, 30),
        )
    except ProviderNotFoundError:
        return

    raise AssertionError("get_prices() accepted missing provider currency")


def test_get_prices_uses_currency_from_company_profile():
    from datetime import date as date_cls

    def handler(request: httpx.Request) -> httpx.Response:
        path = request.url.path

        if path.endswith("/fundamentals/AAPL"):
            return httpx.Response(
                200,
                json={
                    "General": {
                        "Name": "Apple Inc.",
                        "Exchange": "XNAS",
                        "CountryISO": "US",
                        "Sector": "Technology",
                        "Industry": "Consumer Electronics",
                        "CurrencyCode": "EUR",
                        "ISIN": "US0378331005",
                    }
                },
            )

        if path.endswith("/eod/AAPL"):
            return httpx.Response(
                200,
                json=[
                    {
                        "date": "2025-09-30",
                        "close": 228.2,
                    }
                ],
            )

        raise AssertionError(f"unexpected path {path}")

    adapter = EODHDAdapter(
        api_key="fake",
        transport=_mock_transport(handler),
    )

    profile = adapter.get_company_profile("AAPL")

    assert profile.currency == "EUR"
    assert adapter._profile_currency == "EUR"

    bars = adapter.get_prices(
        "AAPL",
        date_cls(2025, 9, 1),
        date_cls(2025, 9, 30),
    )

    assert len(bars) == 1
    assert bars[0].currency == "EUR"


def test_get_prices_parses_valid_bars():
    from datetime import date as date_cls

    def handler(request: httpx.Request) -> httpx.Response:
        assert request.url.path.endswith("/eod/AAPL")
        return httpx.Response(200, json=FAKE_EOD_RESPONSE)

    adapter = EODHDAdapter(api_key="fake", transport=_mock_transport(handler))
    _prime_eodhd_price_currency(adapter)
    bars = adapter.get_prices("AAPL", date_cls(2025, 9, 1), date_cls(2025, 9, 30))
    assert len(bars) == 1
    assert bars[0].close == 228.2
    assert bars[0].volume == 45000000
    assert bars[0].date.isoformat() == "2025-09-30"


def test_get_prices_missing_optional_fields_default_to_none():
    from datetime import date as date_cls

    # only "date" and "close" are ever required by ProviderPriceBar construction here (close has
    # no .get fallback); everything else the adapter reads via .get(...) and must default to None
    # rather than KeyError.
    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(200, json=[{"date": "2025-09-30", "close": 228.2}])

    adapter = EODHDAdapter(api_key="fake", transport=_mock_transport(handler))
    _prime_eodhd_price_currency(adapter)
    bars = adapter.get_prices("AAPL", date_cls(2025, 9, 1), date_cls(2025, 9, 30))
    assert len(bars) == 1
    assert bars[0].open is None
    assert bars[0].high is None
    assert bars[0].low is None
    assert bars[0].adjusted_close is None
    assert bars[0].volume is None


def test_get_prices_wrong_type_in_close_field_is_rejected():
    from datetime import date as date_cls

    # HISTORICAL NOTE, preserved. Part A7 pinned the OPPOSITE behaviour: `ProviderPriceBar` is a
    # plain @dataclass, Python does not enforce `close: float` at construction, and get_prices()
    # did no coercion -- so `"not-a-number"` was passed straight through as a str. A7 documented
    # that as a real gap and deliberately did not fix it, because fixing one adapter method while
    # FMP's equivalent stayed unvalidated would have made the layer MORE inconsistent, not less.
    # UPDATED (Part D -- docs/AUDIT_VALIDATION_D.md): both adapters now share
    # app/adapters/validation.py. The row is rejected with a recorded reason instead.
    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(200, json=[{"date": "2025-09-30", "close": "not-a-number"}])

    adapter = EODHDAdapter(api_key="fake", transport=_mock_transport(handler))
    _prime_eodhd_price_currency(adapter)
    bars = adapter.get_prices("AAPL", date_cls(2025, 9, 1), date_cls(2025, 9, 30))
    assert bars == []
    report = adapter.last_price_validation
    assert report.rows_seen == 1 and report.rows_accepted == 0
    assert any("not parseable as a number" in i.reason for i in report.errors)


def test_get_prices_numeric_string_is_coerced_not_rejected():
    """Providers really do return `"227.0"`. Narrow coercion is the point -- rejecting a valid
    price because it arrived as a string would be its own kind of data loss."""
    from datetime import date as date_cls

    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(200, json=[{"date": "2025-09-30", "close": "227.5"}])

    adapter = EODHDAdapter(api_key="fake", transport=_mock_transport(handler))
    _prime_eodhd_price_currency(adapter)
    bars = adapter.get_prices("AAPL", date_cls(2025, 9, 1), date_cls(2025, 9, 30))
    assert len(bars) == 1 and bars[0].close == 227.5


def test_get_prices_empty_response():
    from datetime import date as date_cls

    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(200, json=[])

    adapter = EODHDAdapter(api_key="fake", transport=_mock_transport(handler))
    _prime_eodhd_price_currency(adapter)
    bars = adapter.get_prices("AAPL", date_cls(2025, 9, 1), date_cls(2025, 9, 30))
    assert bars == []


def test_get_prices_partial_response_keeps_the_good_bars_and_reports_the_bad_one():
    from datetime import date as date_cls

    # HISTORICAL NOTE, preserved. Part A7 pinned the OPPOSITE behaviour: one bar missing "close"
    # raised KeyError and the ENTIRE batch was lost, including every valid bar around it. A7
    # recorded it as a real limitation and left it for a resilience pass rather than fixing one
    # adapter in isolation. UPDATED (Part D): reject the row, not the batch, and say why.
    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(200, json=[
            {"date": "2025-09-29", "close": 227.0},
            {"date": "2025-09-30"},  # missing "close" entirely
            {"date": "2025-10-01", "close": 229.0},
        ])

    adapter = EODHDAdapter(api_key="fake", transport=_mock_transport(handler))
    _prime_eodhd_price_currency(adapter)
    bars = adapter.get_prices("AAPL", date_cls(2025, 9, 1), date_cls(2025, 10, 1))
    assert [b.close for b in bars] == [227.0, 229.0]
    report = adapter.last_price_validation
    assert report.rows_seen == 3 and report.rows_accepted == 2
    assert report.rows_rejected == 1
    assert abs(report.rejection_rate - 1 / 3) < 1e-12


def test_get_prices_row_without_a_date_is_rejected():
    from datetime import date as date_cls

    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(200, json=[{"close": 227.0}])

    adapter = EODHDAdapter(api_key="fake", transport=_mock_transport(handler))
    _prime_eodhd_price_currency(adapter)
    assert adapter.get_prices("AAPL", date_cls(2025, 9, 1), date_cls(2025, 9, 30)) == []
    assert any("no date" in i.reason for i in adapter.last_price_validation.errors)


def test_get_prices_negative_price_is_rejected():
    from datetime import date as date_cls

    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(200, json=[{"date": "2025-09-30", "close": -5.0}])

    adapter = EODHDAdapter(api_key="fake", transport=_mock_transport(handler))
    _prime_eodhd_price_currency(adapter)
    assert adapter.get_prices("AAPL", date_cls(2025, 9, 1), date_cls(2025, 9, 30)) == []
    assert any("strictly positive" in i.reason for i in adapter.last_price_validation.errors)


def test_get_prices_bad_volume_does_not_cost_the_price():
    """A price is the field downstream code needs; an unusable volume is not a reason to throw it
    away. The volume becomes None and a WARNING is recorded."""
    from datetime import date as date_cls

    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(200, json=[{"date": "2025-09-30", "close": 227.0, "volume": -100}])

    adapter = EODHDAdapter(api_key="fake", transport=_mock_transport(handler))
    _prime_eodhd_price_currency(adapter)
    bars = adapter.get_prices("AAPL", date_cls(2025, 9, 1), date_cls(2025, 9, 30))
    assert len(bars) == 1 and bars[0].close == 227.0 and bars[0].volume is None
    assert any("volume cannot be negative" in i.reason
               for i in adapter.last_price_validation.warnings)


# --- list_universe -------------------------------------------------------------------------------

def test_list_universe_parses_codes_valid():
    def handler(request: httpx.Request) -> httpx.Response:
        assert request.url.path.endswith("/exchange-symbol-list/US")
        return httpx.Response(200, json=[{"Code": "AAPL"}, {"Code": "MSFT"}])

    adapter = EODHDAdapter(api_key="fake", transport=_mock_transport(handler))
    tickers = adapter.list_universe(exchange_mic="US")
    assert tickers == ["AAPL", "MSFT"]


def test_list_universe_records_rows_it_drops_instead_of_dropping_them_silently():
    # HISTORICAL NOTE, preserved. Part A7 pinned this as silently dropping rows without a "Code"
    # key -- the exact OPPOSITE policy to get_prices(), which failed the whole batch over one bad
    # row. A7 recorded the inconsistency between two methods of the same adapter as an input to
    # Part D. UPDATED (Part D): same policy in both, and the drop is now recorded.
    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(200, json=[
            {"Code": "AAPL"}, {"Name": "no code here"}, {"Code": "MSFT"},
        ])

    adapter = EODHDAdapter(api_key="fake", transport=_mock_transport(handler))
    assert adapter.list_universe(exchange_mic="US") == ["AAPL", "MSFT"]
    report = adapter.last_universe_validation
    assert report.rows_seen == 3 and report.rows_accepted == 2
    assert any("no ticker code" in i.reason for i in report.errors)


def test_list_universe_no_exchange_mic_returns_empty():
    adapter = EODHDAdapter(api_key="fake", transport=_mock_transport(lambda r: httpx.Response(200, json=[])))
    assert adapter.list_universe() == []  # no exchange_mic given -- never makes a request at all


def test_list_universe_empty_response():
    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(200, json=[])

    adapter = EODHDAdapter(api_key="fake", transport=_mock_transport(handler))
    assert adapter.list_universe(exchange_mic="US") == []


# --- Statement methods: documented NotImplementedError (ties to Part A6's caught bug) -----------

def test_get_income_statements_raises_not_implemented():
    adapter = EODHDAdapter(api_key="fake", transport=_mock_transport(lambda r: httpx.Response(200, json={})))
    try:
        adapter.get_income_statements("AAPL")
        raised = False
    except NotImplementedError:
        raised = True
    assert raised


def test_get_balance_sheets_raises_not_implemented():
    adapter = EODHDAdapter(api_key="fake", transport=_mock_transport(lambda r: httpx.Response(200, json={})))
    try:
        adapter.get_balance_sheets("AAPL")
        raised = False
    except NotImplementedError:
        raised = True
    assert raised


def test_get_cash_flows_raises_not_implemented():
    adapter = EODHDAdapter(api_key="fake", transport=_mock_transport(lambda r: httpx.Response(200, json={})))
    try:
        adapter.get_cash_flows("AAPL")
        raised = False
    except NotImplementedError:
        raised = True
    assert raised


def test_get_estimates_raises_not_implemented():
    adapter = EODHDAdapter(api_key="fake", transport=_mock_transport(lambda r: httpx.Response(200, json={})))
    try:
        adapter.get_estimates("AAPL")
        raised = False
    except NotImplementedError:
        raised = True
    assert raised


def test_get_dividends_raises_not_implemented():
    adapter = EODHDAdapter(api_key="fake", transport=_mock_transport(lambda r: httpx.Response(200, json={})))
    try:
        adapter.get_dividends("AAPL")
        raised = False
    except NotImplementedError:
        raised = True
    assert raised


ALL_TESTS = [obj for name, obj in list(globals().items()) if name.startswith("test_") and callable(obj)]

if __name__ == "__main__":
    passed, failed = 0, []
    for fn in ALL_TESTS:
        try:
            fn()
            passed += 1
            print(f"PASS  {fn.__name__}")
        except AssertionError as e:
            failed.append(fn.__name__)
            print(f"FAIL  {fn.__name__}: {e}")
    print(f"\n{passed}/{len(ALL_TESTS)} passed")
    if failed:
        raise SystemExit(1)
