from __future__ import annotations

import zipfile
from pathlib import Path

import pytest

from jarvis_finance.imports.instrument_candidates import (
    CandidateInput,
    confirm_candidate,
    deterministic_parse_docx,
    import_confirmed_candidate_snapshot,
    qwen_extract_candidates,
    qwen_resolve_staged_candidates,
    resolve_candidate,
    stage_docx_candidates,
    upsert_candidate,
)
from jarvis_finance.market_data.catalog import CatalogEntryInput, upsert_catalog_entry
from jarvis_finance.storage.database import connect_memory
from jarvis_finance.storage.migrations import apply_migrations


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','PostFinance','broker','CHF','now')")
    conn.execute("INSERT INTO accounts(account_id,platform_id,account_name,account_type,currency,created_at) VALUES('a1','p1','Depot','brokerage','CHF','now')")
    return conn


def make_docx(path: Path, lines: list[str]) -> Path:
    body = "".join(f"<w:p><w:r><w:t>{line}</w:t></w:r></w:p>" for line in lines)
    xml = f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>{body}</w:body></w:document>'''
    with zipfile.ZipFile(path, "w") as zf:
        zf.writestr("word/document.xml", xml)
    return path


def candidate(**overrides) -> CandidateInput:
    data = dict(
        source_file="broker.docx",
        platform="postfinance",
        source_row_number="1",
        raw_name="Sample ETF",
        raw_ticker="SAMP",
        raw_isin="",
        raw_currency="CHF",
        raw_quantity="1.2345",
        raw_asset_class="etf",
        proposed_name="Sample ETF",
        proposed_ticker="SAMP",
        proposed_currency="CHF",
        proposed_asset_class="etf",
        mapping_status="needs_manual_review",
    )
    data.update(overrides)
    return CandidateInput(**data)


def test_deterministic_parser_creates_candidates(tmp_path: Path) -> None:
    docx = make_docx(tmp_path / "Postfinance.docx", ["Sample ETF CH0000000001 CHF 12.345"])
    rows = deterministic_parse_docx(docx, platform="postfinance")
    assert len(rows) == 1
    assert rows[0].raw_isin == "CH0000000001"
    assert rows[0].mapping_status == "exact_isin_match"


def test_qwen_extraction_creates_only_candidates_no_bookings(monkeypatch) -> None:
    captured = {}
    class FakeResponse:
        def __enter__(self): return self
        def __exit__(self, *args): return False
        def read(self):
            return b'{"choices":[{"message":{"content":"{\\\"positions\\\":[{\\\"name\\\":\\\"Q ETF\\\",\\\"ticker\\\":\\\"QETF\\\",\\\"currency\\\":\\\"CHF\\\",\\\"quantity\\\":\\\"1.23\\\",\\\"asset_class\\\":\\\"etf\\\",\\\"row_number\\\":2,\\\"confidence\\\":\\\"medium\\\"}]}"}}]}'
    def fake_urlopen(req, **kwargs):
        captured["body"] = req.data.decode("utf-8")
        return FakeResponse()
    monkeypatch.setattr("jarvis_finance.imports.instrument_candidates.request.urlopen", fake_urlopen)
    conn = setup_conn()
    rows, available = qwen_extract_candidates(["Q ETF CHF 1.23"], source_file="True.docx", platform="true_wealth")
    assert available is True
    assert len(rows) == 1
    assert "1.23" not in captured["body"]
    assert rows[0].raw_quantity == ""
    upsert_candidate(conn, rows[0])
    assert conn.execute("SELECT COUNT(*) AS c FROM instrument_import_candidates").fetchone()["c"] == 1
    assert conn.execute("SELECT COUNT(*) AS c FROM transactions").fetchone()["c"] == 0


def test_candidate_without_isin_stays_needs_manual_review() -> None:
    conn = setup_conn()
    cid = upsert_candidate(conn, candidate(raw_isin="", proposed_isin="", proposed_exchange=""))
    result = resolve_candidate(conn, cid)
    assert result["mapping_status"] == "needs_manual_review"


def test_candidate_with_isin_can_be_exact_isin_match() -> None:
    conn = setup_conn()
    upsert_catalog_entry(conn, CatalogEntryInput(asset_class="etf", name="Sample ETF", isin="CH0000000001", ticker="SAMP", exchange="SWX", trading_currency="CHF", instrument_currency="CHF"), note="test")
    cid = upsert_candidate(conn, candidate(raw_isin="CH0000000001", proposed_isin="CH0000000001"))
    result = resolve_candidate(conn, cid)
    assert result["mapping_status"] == "exact_isin_match"


def test_ticker_alone_does_not_confirm_or_import() -> None:
    conn = setup_conn()
    cid = upsert_candidate(conn, candidate(raw_isin="", proposed_isin="", proposed_exchange="", proposed_ticker="SAMP"))
    with pytest.raises(ValueError):
        confirm_candidate(conn, cid, note="manual review confirms", proposed_ticker="SAMP")
    with pytest.raises(ValueError):
        import_confirmed_candidate_snapshot(conn, cid, account_id="a1", snapshot_date="2025-12-31", note="no", confirm=True)


def test_manual_confirmation_sets_confirmed() -> None:
    conn = setup_conn()
    cid = upsert_candidate(conn, candidate())
    confirm_candidate(conn, cid, note="manual review confirms", proposed_ticker="SAMP", proposed_exchange="SWX", proposed_currency="CHF")
    row = conn.execute("SELECT mapping_status FROM instrument_import_candidates WHERE candidate_id=?", (cid,)).fetchone()
    assert row["mapping_status"] == "confirmed"
    assert conn.execute("SELECT COUNT(*) AS c FROM audit_log WHERE action='confirm_candidate'").fetchone()["c"] == 1


def test_confirmed_candidate_can_create_initial_snapshot_with_uncertain_cost_basis() -> None:
    conn = setup_conn()
    cid = upsert_candidate(conn, candidate(raw_isin="CH0000000001", proposed_isin="CH0000000001"))
    confirm_candidate(conn, cid, note="manual review confirms", proposed_isin="CH0000000001", proposed_ticker="SAMP", proposed_exchange="SWX", proposed_currency="CHF")
    tx_id = import_confirmed_candidate_snapshot(conn, cid, account_id="a1", snapshot_date="2025-12-31", note="approved import", confirm=True)
    row = conn.execute("SELECT transaction_type, source_type, quantity FROM transactions WHERE transaction_id=?", (tx_id,)).fetchone()
    assert row["transaction_type"] == "initial_position_snapshot"
    assert row["source_type"] == "broker_import_reviewed_snapshot"
    assert row["quantity"] == "1.2345"
    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='cost_basis_uncertain'").fetchone()["c"] == 1
    assert conn.execute("SELECT COUNT(*) AS c FROM audit_log WHERE action='import_confirmed_candidate_snapshot'").fetchone()["c"] == 1


def test_unconfirmed_candidate_cannot_productively_import() -> None:
    conn = setup_conn()
    cid = upsert_candidate(conn, candidate())
    with pytest.raises(ValueError):
        import_confirmed_candidate_snapshot(conn, cid, account_id="a1", snapshot_date="2025-12-31", note="blocked", confirm=True)
    assert conn.execute("SELECT COUNT(*) AS c FROM transactions").fetchone()["c"] == 0


def test_stage_docx_candidates_dry_run_writes_no_rows(tmp_path: Path) -> None:
    conn = setup_conn()
    docx = make_docx(tmp_path / "True_Wealth.docx", ["Sample ETF CH0000000001 CHF 12.345"])
    result = stage_docx_candidates(conn, [docx], dry_run=True)
    assert result.candidates_total == 1
    assert conn.execute("SELECT COUNT(*) AS c FROM instrument_import_candidates").fetchone()["c"] == 0


def test_no_real_data_in_repo_git_safety_placeholder(tmp_path: Path) -> None:
    repo_docx = tmp_path / "repo" / "broker.docx"
    repo_docx.parent.mkdir()
    make_docx(repo_docx, ["Synthetic ETF CH0000000001 CHF 1"])
    # The module can parse synthetic fixtures, while the CLI blocks repo files unless explicitly allowed.
    assert deterministic_parse_docx(repo_docx, platform="test")



def test_qwen_resolver_sends_sanitized_candidates_only(monkeypatch) -> None:
    conn = setup_conn()
    cid = upsert_candidate(conn, candidate(raw_quantity="999.123", proposed_exchange=""))
    captured = {}

    class FakeResponse:
        def __enter__(self): return self
        def __exit__(self, *args): return False
        def read(self):
            body = '{"updates":[{"candidate_id":"%s","proposed_name":"Sample ETF","proposed_ticker":"SAMP","proposed_isin":"","proposed_exchange":"","proposed_currency":"CHF","proposed_asset_class":"etf","mapping_status":"needs_manual_review","review_note":"Ticker-only candidate remains manual review."}]}' % cid
            import json
            return json.dumps({"choices": [{"message": {"content": body}}]}).encode()

    def fake_urlopen(req, **kwargs):
        captured["body"] = req.data.decode("utf-8")
        return FakeResponse()

    monkeypatch.setattr("jarvis_finance.imports.instrument_candidates.request.urlopen", fake_urlopen)
    result = qwen_resolve_staged_candidates(conn, model="qwen-3.6-agent")
    assert result.qwen_available is True
    assert result.qwen_used is True
    assert result.confirmed_after == 0
    assert "999.123" not in captured["body"]
    assert "raw_quantity" not in captured["body"]
    row = conn.execute("SELECT mapping_status, review_note FROM instrument_import_candidates WHERE candidate_id=?", (cid,)).fetchone()
    assert row["mapping_status"] == "needs_manual_review"
    assert "manual review" in row["review_note"]


def test_qwen_resolver_does_not_accept_invented_isin(monkeypatch) -> None:
    conn = setup_conn()
    cid = upsert_candidate(conn, candidate(raw_isin="", proposed_isin="", proposed_exchange=""))

    class FakeResponse:
        def __enter__(self): return self
        def __exit__(self, *args): return False
        def read(self):
            import json
            body = '{"updates":[{"candidate_id":"%s","proposed_name":"Sample ETF","proposed_ticker":"SAMP","proposed_isin":"CH9999999999","proposed_exchange":"SWX","proposed_currency":"CHF","proposed_asset_class":"etf","mapping_status":"probable","review_note":"Qwen guessed an ISIN."}]}' % cid
            return json.dumps({"choices": [{"message": {"content": body}}]}).encode()

    monkeypatch.setattr("jarvis_finance.imports.instrument_candidates.request.urlopen", lambda *a, **k: FakeResponse())
    result = qwen_resolve_staged_candidates(conn)
    assert result.warnings == 1
    row = conn.execute("SELECT proposed_isin, mapping_status FROM instrument_import_candidates WHERE candidate_id=?", (cid,)).fetchone()
    assert row["proposed_isin"] == ""
    assert row["mapping_status"] == "probable"
