from __future__ import annotations

from decimal import Decimal

import pytest

from jarvis_finance.api.schemas.positions import ContainerConfirmRequest
from jarvis_finance.ledger.performance import (
    annualize_return,
    attribution_bridge_v1,
    price_fx_attribution_v1,
    stable_input_fingerprint,
    ttwror_daily_v1,
    xirr_v1,
)
from jarvis_finance.services.cash_service import ensure_canonical_cash_accounts
from jarvis_finance.services.manual_entry_service import confirm_account
from jarvis_finance.services.performance_scope import (
    set_performance_cashflow_coverage,
    set_performance_scope_classification,
)
from jarvis_finance.storage.database import connect_memory
from jarvis_finance.storage.migrations import apply_migrations, get_schema_version


def test_ttwror_without_external_cashflows():
    result = ttwror_daily_v1([("2025-01-01", Decimal(100)), ("2025-12-31", Decimal(110))], [])
    assert result.value == Decimal("0.1")


def test_ttwror_deposit_at_period_start():
    result = ttwror_daily_v1(
        [("2025-01-01", Decimal(100)), ("2025-01-02", Decimal(165))],
        [("2025-01-01", Decimal(50), "external_deposit")],
    )
    assert result.value == Decimal("0.1")


def test_ttwror_matches_timestamped_cashflow_to_its_calendar_day_valuation():
    result = ttwror_daily_v1(
        [("2025-01-01", Decimal(100)), ("2025-01-02", Decimal(165))],
        [("2025-01-01T12:00:00Z", Decimal(50), "external_deposit")],
    )
    assert result.value == Decimal("0.1")
    assert result.quality.status == "complete"


def test_ttwror_rejects_ambiguous_duplicate_daily_valuations():
    result = ttwror_daily_v1(
        [
            ("2025-01-01T00:00:00Z", Decimal(100)),
            ("2025-01-01T23:00:00Z", Decimal(101)),
            ("2025-01-02", Decimal(110)),
        ],
        [],
    )
    assert result.value is None
    assert result.quality.reasons == ("ambiguous_daily_valuation",)


def test_ttwror_withdrawal_at_period_end():
    result = ttwror_daily_v1(
        [("2025-01-01", Decimal(100)), ("2025-01-02", Decimal(90))],
        [("2025-01-02", Decimal(-20), "external_withdrawal")],
    )
    assert result.value == Decimal("0.1")


def test_ttwror_geometrically_links_multiple_subperiods():
    result = ttwror_daily_v1(
        [
            ("2025-01-01", Decimal(100)),
            ("2025-02-01", Decimal(110)),
            ("2025-03-01", Decimal(176)),
        ],
        [("2025-02-01", Decimal(50), "external_deposit")],
    )
    assert result.value == Decimal("0.21")


def test_ttwror_requires_stored_cashflow_boundary_valuation():
    result = ttwror_daily_v1(
        [("2025-01-01", Decimal(100)), ("2025-03-01", Decimal(176))],
        [("2025-02-01", Decimal(50), "external_deposit")],
    )
    assert result.value is None
    assert result.quality.reasons == ("missing_cashflow_valuation",)


def test_ttwror_rejects_inconsistent_external_cashflow_signs():
    result = ttwror_daily_v1(
        [("2025-01-01", Decimal(100)), ("2025-02-01", Decimal(110))],
        [("2025-01-01", Decimal(-10), "external_deposit")],
    )
    assert result.value is None
    assert result.quality.reasons == ("invalid_cashflow_sign",)


def test_ttwror_rejects_cashflow_that_has_no_return_subperiod():
    result = ttwror_daily_v1(
        [("2025-01-01", Decimal(100)), ("2025-02-01", Decimal(110))],
        [("2025-02-01", Decimal(10), "external_deposit")],
    )
    assert result.value is None
    assert result.quality.reasons == ("cashflow_outside_return_subperiod",)


def test_ttwror_never_treats_buy_sell_dividend_or_fee_as_external():
    result = ttwror_daily_v1([("2025-01-01", Decimal(100)), ("2025-12-31", Decimal(110))], [])
    assert result.value == Decimal("0.1")


def test_ttwror_annualized_is_separate_from_cumulative():
    assert annualize_return(Decimal("0.21"), 730) == Decimal("0.100000000000")
    assert annualize_return(Decimal("0.1"), 365) is None


def test_xirr_matches_documented_excel_reference_example():
    # Microsoft Excel XIRR documentation example: approximately 37.3362535%.
    result = xirr_v1(
        [
            ("2008-01-01", Decimal(-10000)),
            ("2008-03-01", Decimal(2750)),
            ("2008-10-30", Decimal(4250)),
            ("2009-02-15", Decimal(3250)),
            ("2009-04-01", Decimal(2750)),
        ]
    )
    assert result.value is not None
    assert abs(result.value - Decimal("0.373362535")) < Decimal("0.00000001")


def test_xirr_uses_irregular_calendar_days():
    irregular = xirr_v1(
        [
            ("2024-01-01", Decimal(-1000)),
            ("2024-02-17", Decimal(-500)),
            ("2025-04-03", Decimal(1800)),
        ]
    )
    assert irregular.value is not None
    assert irregular.quality.status == "complete"


def test_xirr_without_sign_change_is_unavailable():
    result = xirr_v1([("2025-01-01", Decimal(-100)), ("2026-01-01", Decimal(-10))])
    assert result.value is None
    assert result.quality.reasons == ("insufficient_cashflows",)


def test_xirr_with_ambiguous_sign_pattern_is_unavailable():
    result = xirr_v1(
        [("2025-01-01", Decimal(-100)), ("2025-06-01", Decimal(250)), ("2026-01-01", Decimal(-160))]
    )
    assert result.value is None
    assert result.quality.reasons == ("mwr_multiple_solutions",)


def test_price_fx_interaction_is_deterministically_assigned_to_fx():
    result = price_fx_attribution_v1(
        {"position": (Decimal(100), Decimal("0.9"))},
        {"position": (Decimal(110), Decimal("1.0"))},
    )
    assert result == (Decimal("9.0"), Decimal("11.0"))


def test_attribution_value_bridge_reconciles_within_one_cent():
    bridge = attribution_bridge_v1(
        opening_value=Decimal(100),
        closing_value=Decimal(120),
        net_external_cashflows=Decimal(0),
        market_price=Decimal(15),
        fx=Decimal(3),
        dividends_and_interest=Decimal(4),
        fees=Decimal(1),
        taxes=Decimal(1),
    )
    assert bridge["investment_result"] == Decimal(20)
    assert bridge["unattributed_residual"] == Decimal(0)
    assert bridge["fees"] == Decimal(-1)
    assert bridge["taxes"] == Decimal(-1)
    assert bridge["status"] == "complete"


def test_schema_46_defaults_new_accounts_out_of_performance_and_repeats_as_noop():
    conn = connect_memory()
    apply_migrations(conn)
    conn.execute(
        "INSERT INTO platforms(platform_id,name,platform_type,default_currency,created_at) VALUES ('p','Bank','bank','CHF','2026-01-01T00:00:00Z')"
    )
    conn.execute(
        "INSERT INTO accounts(account_id,platform_id,account_name,account_type,currency,created_at) VALUES ('a','p','New household','bank','CHF','2026-01-01T00:00:00Z')"
    )
    assert (
        conn.execute("SELECT performance_included FROM accounts WHERE account_id='a'").fetchone()[0]
        == 0
    )
    audit_before = conn.execute("SELECT COUNT(*) FROM audit_log").fetchone()[0]
    classification_before = conn.execute(
        "SELECT COUNT(*) FROM performance_scope_classifications"
    ).fetchone()[0]
    apply_migrations(conn)
    assert get_schema_version(conn) == 46
    assert conn.execute("SELECT COUNT(*) FROM audit_log").fetchone()[0] == audit_before
    assert (
        conn.execute("SELECT COUNT(*) FROM performance_scope_classifications").fetchone()[0]
        == classification_before
    )


def test_attribution_unexplained_remainder_remains_visible_and_partial():
    bridge = attribution_bridge_v1(
        opening_value=Decimal(100),
        closing_value=Decimal(120),
        net_external_cashflows=Decimal(0),
        market_price=None,
        fx=None,
        dividends_and_interest=Decimal(2),
        fees=Decimal(1),
        taxes=Decimal(0),
    )
    assert bridge["unattributed_residual"] == Decimal(19)
    assert bridge["status"] == "partial"


def test_total_value_account_does_not_invent_price_or_fx_components():
    bridge = attribution_bridge_v1(
        opening_value=Decimal(100),
        closing_value=Decimal(110),
        net_external_cashflows=Decimal(0),
        market_price=None,
        fx=None,
        dividends_and_interest=Decimal(0),
        fees=Decimal(0),
        taxes=Decimal(0),
    )
    assert bridge["market_price"] is None
    assert bridge["fx"] is None
    assert bridge["unattributed_residual"] == Decimal(10)


def test_identical_cutoff_payload_has_identical_fingerprint():
    payload = {"cutoff": "2026-07-27T12:00:00Z", "values": ["100", "110"]}
    assert stable_input_fingerprint(payload) == stable_input_fingerprint(payload)


def test_performance_scope_migration_defaults_new_accounts_to_excluded_and_repeats_as_noop():
    conn = connect_memory()
    apply_migrations(conn)
    conn.execute(
        "INSERT INTO platforms(platform_id,name,platform_type,created_at) VALUES ('p','Bank','bank','2026-01-01')"
    )
    conn.execute(
        "INSERT INTO accounts(account_id,platform_id,account_name,account_type,created_at) VALUES ('a','p','Household','cash','2026-01-01')"
    )
    assert (
        conn.execute("SELECT performance_included FROM accounts WHERE account_id='a'").fetchone()[0]
        == 0
    )
    before = list(conn.execute("SELECT version,name FROM schema_migrations ORDER BY version"))
    apply_migrations(conn)
    assert (
        list(conn.execute("SELECT version,name FROM schema_migrations ORDER BY version")) == before
    )
    assert get_schema_version(conn) == 46
    assert conn.execute("PRAGMA integrity_check").fetchone()[0] == "ok"


def test_new_account_paths_are_excluded_until_audited_role_classification():
    conn = connect_memory()
    apply_migrations(conn)
    manual = confirm_account(
        conn,
        ContainerConfirmRequest(
            account_name="Ordinary household account",
            account_type="bank",
            platform_name="Synthetic Bank",
            currency="CHF",
            note="test",
            confirm=True,
        ),
    )
    assert conn.execute(
        "SELECT performance_included FROM accounts WHERE account_id=?",
        (manual.entity_id,),
    ).fetchone()[0] == 0
    assert conn.execute(
        "SELECT COUNT(*) FROM performance_scope_classifications WHERE account_id=?",
        (manual.entity_id,),
    ).fetchone()[0] == 0

    ensured = ensure_canonical_cash_accounts(conn, created_by="test")
    assert ensured["created"] == 6
    assert conn.execute(
        "SELECT COUNT(*) FROM accounts WHERE account_type='cash' AND performance_included<>0"
    ).fetchone()[0] == 0
    before = conn.execute(
        "SELECT group_concat(account_id || ':' || performance_included, ',') FROM accounts ORDER BY account_id"
    ).fetchone()[0]
    ensure_canonical_cash_accounts(conn, created_by="test")
    after = conn.execute(
        "SELECT group_concat(account_id || ':' || performance_included, ',') FROM accounts ORDER BY account_id"
    ).fetchone()[0]
    assert after == before

    with pytest.raises(Exception, match="new accounts default outside performance scope"):
        conn.execute(
            """INSERT INTO accounts(account_id,platform_id,account_name,account_type,currency,
                 performance_included,created_at)
               VALUES('bypass',?,'Bypass','brokerage','CHF',1,'2026-01-01')""",
            (conn.execute("SELECT platform_id FROM platforms LIMIT 1").fetchone()[0],),
        )


def test_scope_classification_is_role_gated_idempotent_and_every_change_is_audited():
    conn = connect_memory()
    apply_migrations(conn)
    conn.execute(
        "INSERT INTO platforms(platform_id,name,platform_type,created_at) VALUES('p','Synthetic','broker','2026-01-01')"
    )
    for account_id in ("pf", "tw", "crypto"):
        conn.execute(
            """INSERT INTO accounts(account_id,platform_id,account_name,account_type,currency,
                 performance_included,created_at) VALUES(?, 'p', ?, 'brokerage', 'CHF', 0, '2026-01-01')""",
            (account_id, account_id),
        )
    with pytest.raises(ValueError, match="approved investment role"):
        set_performance_scope_classification(
            conn,
            account_id="pf",
            included=True,
            classification_role="ordinary_bank_account",
            source="test",
            note="must fail",
            classified_at="2026-01-01T00:00:00Z",
        )

    assert set_performance_scope_classification(
        conn,
        account_id="pf",
        included=True,
        classification_role="postfinance_etrading_depot",
        source="test",
        note="approved",
        classified_at="2026-01-01T00:00:01Z",
    )
    audit_count = conn.execute(
        "SELECT COUNT(*) FROM audit_log WHERE entity_type='performance_scope_classification' AND entity_id='pf'"
    ).fetchone()[0]
    assert not set_performance_scope_classification(
        conn,
        account_id="pf",
        included=True,
        classification_role="postfinance_etrading_depot",
        source="test",
        note="repeat",
        classified_at="2026-01-01T00:00:02Z",
    )
    assert conn.execute(
        "SELECT COUNT(*) FROM audit_log WHERE entity_type='performance_scope_classification' AND entity_id='pf'"
    ).fetchone()[0] == audit_count

    assert set_performance_scope_classification(
        conn,
        account_id="pf",
        included=False,
        classification_role="not_in_investment_performance_scope",
        source="test",
        note="excluded",
        classified_at="2026-01-01T00:00:03Z",
    )
    assert set_performance_scope_classification(
        conn,
        account_id="pf",
        included=True,
        classification_role="postfinance_etrading_depot",
        source="test",
        note="included again",
        classified_at="2026-01-01T00:00:04Z",
    )
    assert conn.execute(
        "SELECT COUNT(*) FROM audit_log WHERE entity_type='performance_scope_classification' AND entity_id='pf'"
    ).fetchone()[0] == audit_count + 2

    for account_id, role in (
        ("tw", "canonical_truewealth_total_value"),
        ("crypto", "crypto_portfolio"),
    ):
        assert set_performance_scope_classification(
            conn,
            account_id=account_id,
            included=True,
            classification_role=role,
            source="test",
            note="approved role",
            classified_at=f"2026-01-01T00:00:0{5 if account_id == 'tw' else 6}Z",
        )
    assert dict(
        conn.execute("SELECT account_id,performance_included FROM accounts ORDER BY account_id")
    ) == {"crypto": 1, "pf": 1, "tw": 1}

    with pytest.raises(ValueError, match="approved non-investment role"):
        set_performance_scope_classification(
            conn,
            account_id="pf",
            included=False,
            classification_role="ordinary_bank_account",
            source="test",
            note="must fail",
        )
    conn.execute(
        """INSERT INTO accounts(account_id,platform_id,account_name,account_type,currency,
             performance_included,created_at) VALUES('bogus','p','Bogus','brokerage','CHF',0,'2026-01-01')"""
    )
    conn.execute(
        """INSERT INTO audit_log(audit_id,timestamp,source,action,entity_type,entity_id,
             old_values_json,new_values_json,user_text_note,created_by,created_at)
           VALUES('bogus-audit','2026-01-01','test','other','other','bogus','{}',
             '{"performance_included":1,"classification_role":"postfinance_etrading_depot"}',
             'invalid audit','test','2026-01-01')"""
    )
    with pytest.raises(Exception, match="invalid or unaudited"):
        conn.execute(
            """INSERT INTO performance_scope_classifications(
                 account_id,included,classification_role,decision_version,audit_id,classified_at)
               VALUES('bogus',1,'postfinance_etrading_depot','investment_performance_scope_v1',
                      'bogus-audit','2026-01-01')"""
        )
    with pytest.raises(Exception, match="new matching audit"):
        conn.execute(
            "UPDATE performance_scope_classifications SET classified_at='2026-02-01' WHERE account_id='pf'"
        )
    with pytest.raises(Exception, match="cannot be deleted"):
        conn.execute("DELETE FROM performance_scope_classifications WHERE account_id='pf'")
    with pytest.raises(Exception, match="flag must match audited classification"):
        conn.execute("UPDATE accounts SET performance_included=0 WHERE account_id='pf'")
    assert conn.execute(
        "SELECT performance_included FROM accounts WHERE account_id='pf'"
    ).fetchone()[0] == 1

    assert set_performance_cashflow_coverage(
        conn,
        account_id="pf",
        coverage_from="2025-01-01",
        coverage_to="2025-12-31",
        status="complete",
        source="synthetic_ledger_reconciliation",
        note="Complete source-to-ledger reconciliation",
        recorded_at="2026-01-02T00:00:00Z",
    )
    coverage_audits = conn.execute(
        "SELECT COUNT(*) FROM audit_log WHERE entity_type='performance_cashflow_coverage'"
    ).fetchone()[0]
    assert not set_performance_cashflow_coverage(
        conn,
        account_id="pf",
        coverage_from="2025-01-01",
        coverage_to="2025-12-31",
        status="complete",
        source="synthetic_ledger_reconciliation",
        note="Idempotent repeat",
        recorded_at="2026-01-03T00:00:00Z",
    )
    assert conn.execute(
        "SELECT COUNT(*) FROM audit_log WHERE entity_type='performance_cashflow_coverage'"
    ).fetchone()[0] == coverage_audits
    with pytest.raises(Exception, match="cannot be deleted"):
        conn.execute("DELETE FROM performance_cashflow_coverage WHERE account_id='pf'")
