from __future__ import annotations

from collections.abc import Iterator
from datetime import date
from decimal import Decimal
import sqlite3
from sqlite3 import Connection

from fastapi.testclient import TestClient

from jarvis_finance.api.dependencies import get_db
from jarvis_finance.api.main import create_app
from jarvis_finance.api.schemas.market import QuoteRefreshRequest
from jarvis_finance.market_data.prices import EquityPriceQuote
from jarvis_finance.services.equity_service import get_equity_summary, list_equity_positions
from jarvis_finance.services.market_service import _quality_from_error, refresh_equity_quote, refresh_equity_quotes_batch
from jarvis_finance.services.portfolio_analytics import confirmed_canonical_positions, run_daily_market_valuation
from jarvis_finance.services.portfolio_advisor import get_portfolio_advisor_snapshot
from jarvis_finance.storage.migrations import apply_migrations

NOW = "2026-07-26T12:00:00Z"
TARGET = date(2026, 7, 24)


def _date_text(value: date | str | None) -> str:
    return value.isoformat() if isinstance(value, date) else str(value or TARGET.isoformat())


def _connect() -> Connection:
    conn = sqlite3.connect(":memory:", check_same_thread=False)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA foreign_keys=ON")
    apply_migrations(conn)
    conn.execute("INSERT INTO platforms(platform_id,name,platform_type,country,default_currency,created_at) VALUES('postfinance','PostFinance','broker','CH','CHF',?)", (NOW,))
    conn.execute("INSERT INTO accounts(account_id,platform_id,account_name,account_type,currency,created_at) VALUES('etrading','postfinance','Manual Portfolio','brokerage','CHF',?)", (NOW,))
    return conn


def _client(conn: Connection) -> TestClient:
    app = create_app()
    def override() -> Iterator[Connection]:
        yield conn
    app.dependency_overrides[get_db] = override
    return TestClient(app)


def _seed_positions(conn: Connection, count: int, *, transactions: bool = False) -> None:
    for index in range(count):
        instrument_id = f"inst-{index:02d}"
        ticker = f"T{index:02d}"
        isin = f"CH{index:010d}"
        conn.execute("INSERT INTO instruments(instrument_id,asset_class,name,ticker,isin,exchange,currency,provider_symbol,data_provider_primary,created_at) VALUES(?,?,?,?,?,'SIX','CHF',?,'fmp',?)", (instrument_id, "stock", f"Position {index:02d}", ticker, isin, f"{ticker}.SW", NOW))
        conn.execute("INSERT INTO instrument_price_mappings(mapping_id,instrument_id,isin,ticker,exchange,currency,provider,provider_symbol,provider_market,trading_currency,mapping_status,confidence,created_at) VALUES(?,?,?,?,?,'CHF','fmp',?,'SIX','CHF','mapped','1',?)", (f"map-{index:02d}", instrument_id, isin, ticker, "SIX", f"{ticker}.SW", NOW))
        if transactions:
            conn.execute("INSERT INTO transactions(transaction_id,transaction_type,account_id,instrument_id,trade_date,quantity,currency_original,source_type,source_id,row_hash,is_confirmed,quality_status,created_at) VALUES(?, 'initial_position_snapshot','etrading',?,'2026-07-01','1','CHF','confirmed',?,?,1,'ok',?)", (f"tx-{index:02d}", instrument_id, f"source-{index:02d}", f"hash-{index:02d}", NOW))
    conn.commit()


class FreshProvider:
    name = "mock"
    def __init__(self) -> None:
        self.calls: list[str] = []
    def get_price(self, provider_symbol: str, *, price_date: date | None = None) -> EquityPriceQuote:
        self.calls.append(provider_symbol)
        return EquityPriceQuote(provider_symbol=provider_symbol, currency="CHF", close=Decimal("10"), provider="fmp", provider_market="SIX", price_timestamp=f"{_date_text(price_date)}T12:00:00Z", quality_status="fresh")


def test_batch_processes_all_22_and_resume_skips_fresh_cache(monkeypatch) -> None:
    conn = _connect()
    _seed_positions(conn, 22)
    provider = FreshProvider()
    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: provider)
    result = refresh_equity_quotes_batch(conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0, max_retries=0))
    assert result.total == result.updated == result.valued == result.coverage_total == 22
    assert result.complete is True
    assert len(provider.calls) == 22
    assert conn.execute("SELECT COUNT(*) FROM market_prices WHERE price_date='2026-07-24' AND close IS NOT NULL").fetchone()[0] == 22

    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: (_ for _ in ()).throw(AssertionError("fresh prices must be skipped")))
    resumed = refresh_equity_quotes_batch(conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0, max_retries=0))
    assert resumed.cached == 22
    assert resumed.updated == 0
    assert resumed.complete is True


def test_batch_does_not_treat_wrong_currency_cache_as_coverage(monkeypatch) -> None:
    conn = _connect()
    _seed_positions(conn, 1)
    conn.execute("""INSERT INTO market_prices(market_price_id,instrument_id,price_date,close,currency,provider,provider_symbol,provider_market,quality_status,created_at)
                    VALUES('wrong-cache','inst-00',?,'99','USD','fmp','T00.SW','SIX','fresh',?)""", (TARGET.isoformat(), NOW))
    conn.commit()
    provider = FreshProvider()
    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: provider)
    result = refresh_equity_quotes_batch(conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0, max_retries=0))
    assert result.cached == 0 and result.updated == 1 and provider.calls == ["T00.SW"]

    wrong_exchange_conn = _connect()
    _seed_positions(wrong_exchange_conn, 1)
    wrong_exchange_conn.execute("""INSERT INTO market_prices(market_price_id,instrument_id,price_date,close,currency,provider,provider_symbol,provider_market,quality_status,created_at)
                    VALUES('wrong-exchange-cache','inst-00',?,'99','CHF','fmp','T00.SW','NASDAQ','fresh',?)""", (TARGET.isoformat(), NOW))
    wrong_exchange_conn.commit()
    exchange_provider = FreshProvider()
    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: exchange_provider)
    exchange_result = refresh_equity_quotes_batch(wrong_exchange_conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0, max_retries=0))
    assert exchange_result.cached == 0 and exchange_result.updated == 1 and exchange_provider.calls == ["T00.SW"]


def test_rate_limit_retries_without_marking_instrument_inactive(monkeypatch) -> None:
    conn = _connect()
    _seed_positions(conn, 1)
    class RetryProvider(FreshProvider):
        def get_price(self, provider_symbol: str, *, price_date: date | None = None) -> EquityPriceQuote:
            self.calls.append(provider_symbol)
            if len(self.calls) == 1:
                return EquityPriceQuote(provider_symbol=provider_symbol, currency="CHF", close=None, provider="fmp", provider_market="SIX", price_timestamp=f"{_date_text(price_date)}T12:00:00Z", quality_status="rate_limited", error_message="fmp_rate_limited")
            return EquityPriceQuote(provider_symbol=provider_symbol, currency="CHF", close=Decimal("10"), provider="fmp", provider_market="SIX", price_timestamp=f"{_date_text(price_date)}T12:00:00Z", quality_status="fresh")
    provider = RetryProvider()
    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: provider)
    result = refresh_equity_quotes_batch(conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0, max_retries=1))
    assert result.updated == 1 and result.results[0]["attempts"] == 2
    assert conn.execute("SELECT instrument_status FROM instruments WHERE instrument_id='inst-00'").fetchone()[0] != "suspected_inactive"


def test_excluded_instrument_is_not_fetched_or_valued(monkeypatch) -> None:
    conn = _connect()
    _seed_positions(conn, 1, transactions=True)
    conn.execute("UPDATE instruments SET valuation_policy='exclude_from_auto_price_update' WHERE instrument_id='inst-00'")
    conn.commit()
    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda *_args: (_ for _ in ()).throw(AssertionError("excluded instrument contacted provider")))
    direct = refresh_equity_quote(conn, "inst-00", QuoteRefreshRequest(provider="auto", price_date=TARGET.isoformat()))
    batch = refresh_equity_quotes_batch(conn, QuoteRefreshRequest(provider="auto", limit=100, price_date=TARGET.isoformat(), pacing_seconds=0))
    assert direct.close is None and direct.quality_status == "missing"
    assert batch.total == 0 and confirmed_canonical_positions(conn, as_of=TARGET.isoformat()) == []


def test_provider_error_classes_future_and_currency_guards(monkeypatch) -> None:
    assert _quality_from_error("fmp_endpoint_restricted") == "plan_restricted"
    assert _quality_from_error("fmp_auth_error") == "auth_error"
    assert _quality_from_error("fmp_rate_limited") == "rate_limited"
    conn = _connect()
    _seed_positions(conn, 1)

    future = EquityPriceQuote(provider_symbol="T00.SW", currency="CHF", close=Decimal("11"), provider="yfinance", provider_market="XSWX", price_timestamp="2026-07-25T12:00:00Z", quality_status="fresh")
    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: type("P", (), {"get_price": lambda self, *_args, **_kwargs: future})())
    rejected = refresh_equity_quote(conn, "inst-00", QuoteRefreshRequest(provider="auto", price_date=TARGET.isoformat()))
    assert rejected.quality_status == "future_price_rejected"
    assert conn.execute("SELECT COUNT(*) FROM market_prices").fetchone()[0] == 0

    mismatch = EquityPriceQuote(provider_symbol="T00.SW", currency="USD", close=Decimal("11"), provider="yfinance", provider_market="XSWX", price_timestamp="2026-07-24T12:00:00Z", quality_status="fresh")
    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda _name: type("P", (), {"get_price": lambda self, *_args, **_kwargs: mismatch})())
    rejected = refresh_equity_quote(conn, "inst-00", QuoteRefreshRequest(provider="auto", price_date=TARGET.isoformat()))
    assert rejected.quality_status == "currency_mismatch"
    assert conn.execute("SELECT COUNT(*) FROM market_prices").fetchone()[0] == 0


def test_unknown_values_remain_null_and_partial_summary_lists_all_missing() -> None:
    conn = _connect()
    empty = get_equity_summary(conn)
    assert empty.status == "unavailable" and empty.coverage_complete is False and empty.equity_value_chf is None
    _seed_positions(conn, 3, transactions=True)
    class PartialProvider:
        name = "fmp"
        def get_price(self, provider_symbol: str, *, price_date: date | None = None) -> EquityPriceQuote:
            close = Decimal("10") if provider_symbol == "T00.SW" else None
            return EquityPriceQuote(provider_symbol=provider_symbol, currency="CHF", close=close, provider="fmp", provider_market="SIX", price_timestamp=f"{_date_text(price_date)}T12:00:00Z", quality_status="fresh" if close else "endpoint_restricted", error_message=None if close else "fmp_endpoint_restricted")
    result = run_daily_market_valuation(conn, as_of=TARGET.isoformat(), price_providers={"fmp": PartialProvider()})  # type: ignore[dict-item]
    assert result.status == "partial"
    rows = list_equity_positions(conn)
    assert len(rows) == 3
    assert sum(row.market_value_chf is None for row in rows) == 2
    summary = get_equity_summary(conn)
    assert summary.coverage_complete is False
    assert summary.equity_value_chf is None
    assert summary.valued_partial_chf == "10.00"
    assert summary.unvalued_positions == 2
    assert summary.unvalued_tickers == ["T01", "T02"]
    assert all(row.portfolio_share_pct is None for row in rows)


def test_stale_snapshot_values_remain_unknown_and_global_signals_are_suppressed() -> None:
    conn = _connect()
    _seed_positions(conn, 1, transactions=True)

    class StaleProvider:
        name = "fmp"

        def get_price(self, provider_symbol: str, *, price_date: date | None = None) -> EquityPriceQuote:
            return EquityPriceQuote(provider_symbol=provider_symbol, currency="CHF", close=Decimal("10"), provider="fmp", provider_market="SIX", price_timestamp="2026-07-20T12:00:00Z", quality_status="fresh")

    result = run_daily_market_valuation(conn, as_of=TARGET.isoformat(), price_providers={"fmp": StaleProvider()})  # type: ignore[dict-item]
    assert result.status == "partial" and "stale_price" in result.reason_codes
    position = list_equity_positions(conn)[0]
    summary = get_equity_summary(conn)
    assert position.market_value_chf is None and position.status_code == "stale_price"
    assert summary.coverage_complete is False and summary.equity_value_chf is None
    assert get_portfolio_advisor_snapshot(conn).investment_signals == []


def test_equity_get_endpoints_do_not_invoke_provider(monkeypatch) -> None:
    conn = _connect()
    _seed_positions(conn, 1, transactions=True)
    monkeypatch.setattr("jarvis_finance.services.market_service.equity_price_provider_by_name", lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("GET render called provider")))
    client = _client(conn)
    assert client.get("/api/equity/positions").status_code == 200
    assert client.get("/api/equity/summary").status_code == 200
