from __future__ import annotations

from decimal import Decimal

import pytest

from jarvis_finance.dashboard import data
from jarvis_finance.dashboard.pages import page_13_equity_manage
from jarvis_finance.fx.overrides import set_manual_fx_override
from jarvis_finance.market_data.candidates import (
    LookupUnavailable,
    ProviderCandidate,
    dedupe_missing_provider_symbol_alerts,
    export_mapping_decision_pack,
    export_mapping_review_template,
    generate_mapping_candidates,
    rank_candidate,
    recalculate_candidate_rankings,
    reject_mapping_candidate,
    select_mapping_candidate,
)
from jarvis_finance.market_data.instruments import ensure_instrument_metadata_quality
from jarvis_finance.market_data.prices import refresh_market_prices
from jarvis_finance.storage.database import connect_memory
from jarvis_finance.storage.migrations import apply_migrations


class StaticLookupProvider:
    name = "static"

    def __init__(self, candidates_by_isin=None, *, fail=False):
        self.candidates_by_isin = candidates_by_isin or {}
        self.fail = fail

    def lookup(self, *, isin, name, ticker, exchange, currency):
        if self.fail:
            raise LookupUnavailable("lookup down")
        return list(self.candidates_by_isin.get(isin, []))


class RaisingPriceProvider:
    name = "raising"

    def get_price(self, symbol: str):
        raise AssertionError("price provider should not be called without confirmed mappings")


class FakeTab:
    def __enter__(self):
        return self

    def __exit__(self, *args):
        return False


class FakeSt:
    def title(self, *args, **kwargs): pass
    def caption(self, *args, **kwargs): pass
    def warning(self, *args, **kwargs): pass
    def subheader(self, *args, **kwargs): pass
    def write(self, *args, **kwargs): pass
    def success(self, *args, **kwargs): pass
    def error(self, *args, **kwargs): pass
    def info(self, *args, **kwargs): pass
    def divider(self, *args, **kwargs): pass
    def dataframe(self, *args, **kwargs): pass
    def table(self, *args, **kwargs): pass
    def tabs(self, labels): return [FakeTab() for _ in labels]
    def selectbox(self, label, options, **kwargs): return options[0] if options else None
    def text_input(self, *args, **kwargs): return kwargs.get("value", "")
    def text_area(self, *args, **kwargs): return ""
    def date_input(self, *args, **kwargs): return "2025-12-31"
    def checkbox(self, *args, **kwargs): return False
    def expander(self, *args, **kwargs): return FakeTab()
    def button(self, *args, **kwargs): return False


def setup_conn():
    conn = connect_memory()
    apply_migrations(conn)
    conn.execute("INSERT INTO platforms(platform_id,name,platform_type,default_currency,created_at) VALUES('p1','True Wealth','broker','CHF','now')")
    conn.execute("INSERT INTO accounts(account_id,platform_id,account_name,account_type,currency,created_at) VALUES('a1','p1','TW','brokerage','CHF','now')")
    conn.execute("INSERT INTO instruments(instrument_id,asset_class,name,ticker,isin,exchange,currency,created_at) VALUES('inst1','ETF','Global Equity ETF','GETF','CH0000000001',NULL,'USD','now')")
    conn.execute("INSERT INTO transactions(transaction_id,transaction_type,account_id,instrument_id,trade_date,quantity,currency_original,fx_status,source_type,is_confirmed,quality_status,created_at) VALUES('tx1','initial_position_snapshot','a1','inst1','2025-12-31','1','USD','missing','broker_import_reviewed_snapshot',1,'warning','now')")
    return conn


def test_candidate_generator_creates_high_candidate_from_isin_match() -> None:
    conn = setup_conn()
    provider = StaticLookupProvider({
        'CH0000000001': [ProviderCandidate(provider='stooq', provider_symbol='GETF.SW', exchange='SWX', currency='USD', name='Global Equity ETF', isin='CH0000000001', instrument_status='active')]
    })
    result = generate_mapping_candidates(conn, provider=provider)
    row = conn.execute("SELECT * FROM instrument_price_mapping_candidates WHERE instrument_id='inst1'").fetchone()
    mapping_count = conn.execute("SELECT COUNT(*) AS c FROM instrument_price_mappings WHERE mapping_status='mapped'").fetchone()['c']
    assert result.candidates_found == 1
    assert result.high_confidence == 1
    assert row['confidence'] == 'high'
    assert row['review_status'] == 'proposed'
    assert row['candidate_asset_class'] == 'etf'
    assert row['candidate_hedge_status'] == 'unknown'
    assert row['candidate_valuation_policy'] == 'live_price'
    assert mapping_count == 0


def test_name_only_candidate_is_review_required_low_confidence() -> None:
    conn = setup_conn()
    provider = StaticLookupProvider({'CH0000000001': [ProviderCandidate(provider='manual', provider_symbol='UNKNOWN', name='Global Equity ETF')]})
    generate_mapping_candidates(conn, provider=provider)
    row = conn.execute("SELECT confidence,review_status FROM instrument_price_mapping_candidates").fetchone()
    assert row['confidence'] == 'low'
    assert row['review_status'] == 'needs_manual_review'


def test_multiple_candidates_are_manual_review_and_alerted() -> None:
    conn = setup_conn()
    provider = StaticLookupProvider({'CH0000000001': [
        ProviderCandidate(provider='stooq', provider_symbol='GETF.SW', exchange='SWX', isin='CH0000000001'),
        ProviderCandidate(provider='yahoo', provider_symbol='GETF.SW', exchange='SWX', isin='CH0000000001'),
    ]})
    result = generate_mapping_candidates(conn, provider=provider)
    statuses = {r['review_status'] for r in conn.execute("SELECT review_status FROM instrument_price_mapping_candidates").fetchall()}
    assert result.ambiguous_provider_mapping == 1
    assert statuses == {'needs_manual_review'}
    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='ambiguous_provider_mapping' AND status='active'").fetchone()['c'] == 1


def test_no_candidate_keeps_missing_provider_symbol_active() -> None:
    conn = setup_conn()
    result = generate_mapping_candidates(conn, provider=StaticLookupProvider({}))
    assert result.instruments_without_candidate == 1
    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='missing_provider_symbol' AND status='active'").fetchone()['c'] == 1


def test_hedge_and_instrument_status_unknown_alerts() -> None:
    conn = setup_conn()
    ensure_instrument_metadata_quality(conn, instrument_id='inst1')
    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='hedge_status_unknown' AND status='active'").fetchone()['c'] == 1
    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='instrument_status_unknown' AND status='active'").fetchone()['c'] == 1


def test_chf_hedged_candidate_remains_proposed_until_confirmed() -> None:
    conn = setup_conn()
    provider = StaticLookupProvider({'CH0000000001': [ProviderCandidate(provider='stooq', provider_symbol='GETF.SW', exchange='SWX', currency='CHF', name='Global Equity ETF CHF Hedged', isin='CH0000000001', instrument_status='active')]})
    generate_mapping_candidates(conn, provider=provider)
    row = conn.execute("SELECT candidate_is_hedged,candidate_hedged_to_currency,review_status FROM instrument_price_mapping_candidates").fetchone()
    inst = conn.execute("SELECT hedge_status FROM instruments WHERE instrument_id='inst1'").fetchone()
    assert row['candidate_is_hedged'] == 1
    assert row['candidate_hedged_to_currency'] == 'CHF'
    assert row['review_status'] == 'needs_manual_review'
    assert inst['hedge_status'] == 'unknown'


def test_provider_lookup_error_does_not_crash() -> None:
    conn = setup_conn()
    result = generate_mapping_candidates(conn, provider=StaticLookupProvider(fail=True))
    assert result.provider_lookup_unavailable == 1
    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='provider_lookup_unavailable' AND status='active'").fetchone()['c'] == 1


def test_mapping_selection_writes_audit_and_resolves_missing_provider_symbol() -> None:
    conn = setup_conn()
    conn.execute("INSERT INTO alerts(alert_id,priority,category,entity_type,entity_id,rule_id,message,status,created_at) VALUES('mps','warnung','market_data','instrument','inst1','missing_provider_symbol','missing','active','now')")
    provider = StaticLookupProvider({'CH0000000001': [ProviderCandidate(provider='stooq', provider_symbol='GETF.SW', exchange='SWX', currency='USD', isin='CH0000000001', instrument_status='active')]})
    generate_mapping_candidates(conn, provider=provider)
    candidate_id = conn.execute("SELECT candidate_id FROM instrument_price_mapping_candidates").fetchone()['candidate_id']
    mapping_id = select_mapping_candidate(conn, candidate_id=candidate_id, note='manual confirmation')
    assert mapping_id
    assert conn.execute("SELECT COUNT(*) AS c FROM audit_log WHERE action IN ('confirm_instrument_price_mapping','select_mapping_candidate')").fetchone()['c'] >= 2
    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='missing_provider_symbol' AND status='active'").fetchone()['c'] == 0


def test_fx_override_requires_note_and_resolves_missing_fx() -> None:
    conn = setup_conn()
    conn.execute("INSERT INTO alerts(alert_id,priority,category,entity_type,entity_id,rule_id,message,status,created_at) VALUES('fx1','kritisch','fx','instrument','inst1','missing_fx','missing','active','now')")
    with pytest.raises(ValueError):
        set_manual_fx_override(conn, base_currency='USD', quote_currency='CHF', rate_date='2025-12-31', rate=Decimal('0.9'), note='')
    set_manual_fx_override(conn, base_currency='USD', quote_currency='CHF', rate_date='2025-12-31', rate=Decimal('0.9'), note='statement source')
    assert conn.execute("SELECT COUNT(*) AS c FROM audit_log WHERE action='manual_fx_override'").fetchone()['c'] == 1
    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='missing_fx' AND status='active'").fetchone()['c'] == 0


def test_market_price_dry_run_without_confirmed_mapping_does_not_call_provider() -> None:
    conn = setup_conn()
    result = refresh_market_prices(conn, provider=RaisingPriceProvider(), asset_class='etf', limit=3, dry_run=True)
    assert result.total_mappings == 0
    assert result.updated_count == 0


def test_dashboard_review_ui_renders_without_live_api() -> None:
    conn = setup_conn()
    page_13_equity_manage.render(FakeSt(), conn)


def test_missing_provider_symbol_dedupes_per_instrument_only() -> None:
    conn = setup_conn()
    conn.execute("INSERT INTO alerts(alert_id,priority,category,entity_type,entity_id,rule_id,message,status,created_at) VALUES('mps1','warnung','market_data','instrument','inst1','missing_provider_symbol','missing','active','now')")
    conn.execute("INSERT INTO alerts(alert_id,priority,category,entity_type,entity_id,rule_id,message,status,created_at) VALUES('mps2','warnung','market_data','instrument','inst1','missing_provider_symbol','missing','active','now')")
    resolved = dedupe_missing_provider_symbol_alerts(conn)
    assert resolved == 1
    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='missing_provider_symbol' AND status='active'").fetchone()['c'] == 1


def test_mapping_template_contains_candidate_fields(tmp_path) -> None:
    conn = setup_conn()
    provider = StaticLookupProvider({'CH0000000001': [ProviderCandidate(provider='stooq', provider_symbol='GETF.SW', exchange='SWX', currency='USD', isin='CH0000000001')]})
    generate_mapping_candidates(conn, provider=provider)
    out = tmp_path / 'mapping_template.csv'
    summary = export_mapping_review_template(conn, output_path=out)
    text = out.read_text(encoding='utf-8')
    assert summary['instrument_count'] == 1
    assert summary['candidate_rows'] == 1
    assert 'candidate_provider,candidate_provider_symbol,candidate_exchange,candidate_currency,confidence,ranking_score,ranking_reason,risk_flags,recommended_action,hedge_status_candidate,instrument_status_candidate,evidence_source' in text
    assert 'reviewer_decision,reviewer_note' in text


def test_data_quality_center_shows_candidate_review_need() -> None:
    conn = setup_conn()
    provider = StaticLookupProvider({'CH0000000001': [
        ProviderCandidate(provider='stooq', provider_symbol='GETF.SW', exchange='SWX', isin='CH0000000001'),
        ProviderCandidate(provider='yahoo', provider_symbol='GETF.SW', exchange='SWX', isin='CH0000000001'),
    ]})
    generate_mapping_candidates(conn, provider=provider)
    checks = {row['check']: row for row in data.get_data_quality(conn)}
    assert checks['Mapping-Kandidaten unbestätigt']['count'] == '2'
    assert checks['Instrumente mit mehreren Mapping-Kandidaten']['count'] == '1'
    assert checks['ambiguous_provider_mapping']['count'] == '1'


def test_missing_provider_symbol_not_resolved_until_candidate_confirmed() -> None:
    conn = setup_conn()
    generate_mapping_candidates(conn, provider=StaticLookupProvider({}))
    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='missing_provider_symbol' AND status='active'").fetchone()['c'] == 1
    provider = StaticLookupProvider({'CH0000000001': [ProviderCandidate(provider='stooq', provider_symbol='GETF.SW', exchange='SWX', currency='USD', isin='CH0000000001')]})
    generate_mapping_candidates(conn, provider=provider)
    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='missing_provider_symbol' AND status='active'").fetchone()['c'] == 1
    candidate_id = conn.execute("SELECT candidate_id FROM instrument_price_mapping_candidates").fetchone()['candidate_id']
    select_mapping_candidate(conn, candidate_id=candidate_id, note='manual confirmation')
    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='missing_provider_symbol' AND status='active'").fetchone()['c'] == 0
