"""B-44 ledger event layer tests: bank reconciliation, revision chain,
reversal/adjustment, projection rebuild and immutability boundaries."""

from __future__ import annotations

from decimal import Decimal
import sqlite3
import unittest

from bank_importer import ledger_events, matching, subjects
from ledger_helpers import LedgerBase


class BankReconcileTests(LedgerBase):
    def test_paired_eligible_event_creates_pending_subject_only(self) -> None:
        self.pair(self.company_a, self.company_b, "100.00")
        self.assertEqual(1, len(self.eligible()))
        stats = ledger_events.reconcile_bank_events(self.connection, actor=self.admin)
        self.assertEqual(1, stats["created"])
        events = self.ledger_events()
        self.assertEqual(1, len(events))
        revision = self.current(events[0]["id"])
        self.assertEqual("pending_subject", revision["state"])
        self.assertEqual(Decimal("100.00"), Decimal(revision["amount"]))
        self.assertEqual("CNY", revision["currency"])
        # Nothing confirmed yet -> no position event.
        self.assertEqual([], self.position_events())
        # The event is unresolved as subject_review.
        from bank_importer import positions
        unresolved = positions.unresolved_for_company(self.connection, self.company_a, "2026-12-31")
        self.assertEqual("100.00", unresolved["by_reason"]["subject_review"]["gross_amount"])

    def test_unlocked_single_never_enters_ledger(self) -> None:
        self.add_row(
            self.company_a, own_account="6222000000000001",
            cp_account="6222000000000002", expense="100.00",
        )
        matching.reconcile_rows(self.connection, [1])
        self.assertEqual([], self.eligible())
        stats = ledger_events.reconcile_bank_events(self.connection, actor=self.admin)
        self.assertEqual(0, stats["created"])
        self.assertEqual([], self.ledger_events())

    def test_same_company_transfer_never_enters_ledger(self) -> None:
        self.add_row(
            self.company_a, own_account="6222000000000001",
            cp_account="6222000000000001", expense="100.00",
        )
        self.add_row(
            self.company_a, own_account="6222000000000001",
            cp_account="6222000000000001", income="100.00",
        )
        matching.reconcile_rows(self.connection, [1, 2])
        eligible = self.eligible()
        self.assertEqual(0, len([e for e in eligible if e["payer_company_id"] == e["payee_company_id"]]))
        stats = ledger_events.reconcile_bank_events(self.connection, actor=self.admin)
        self.assertEqual(0, stats["created"])

    def test_reconcile_is_idempotent(self) -> None:
        self.pair(self.company_a, self.company_b, "100.00")
        ledger_events.reconcile_bank_events(self.connection, actor=self.admin)
        events = self.ledger_events()
        self.assertEqual(1, len(events))
        before_revisions = self.connection.execute(
            "SELECT COUNT(*) AS n FROM ledger_event_revisions"
        ).fetchone()["n"]
        stats = ledger_events.reconcile_bank_events(self.connection, actor=self.admin)
        self.assertEqual(1, stats["unchanged"])
        after_revisions = self.connection.execute(
            "SELECT COUNT(*) AS n FROM ledger_event_revisions"
        ).fetchone()["n"]
        self.assertEqual(before_revisions, after_revisions)

    def test_reconcile_picks_up_newly_linked_rows_as_new_pending_event(self) -> None:
        # A single observation locked as intercompany -> pending ledger event.
        row_a = self.add_row(
            self.company_a, own_account="6222000000000001",
            cp_account="6222000000000002", expense="100.00",
        )
        matching.reconcile_rows(self.connection, [row_a])
        single = self.connection.execute(
            "SELECT * FROM current_transfer_decisions"
        ).fetchone()
        event_id = single["event_id"]
        decision = self.connection.execute(
            "SELECT * FROM transfer_match_decisions WHERE id = ?",
            (single["decision_id"],),
        ).fetchone()
        matching.apply_manual_decision(
            self.connection, event_id, "assign_participant",
            reason="函证确认对方",
            expected_revision=decision["revision"], request_key="assign-1",
            actor=self.admin,
            participant={"role": "payee", "company_id": self.company_b},
        )
        self.assertEqual(1, len(self.eligible()))
        ledger_events.reconcile_bank_events(self.connection, actor=self.admin)
        self.assertEqual(1, len(self.ledger_events()))

        # The administrator links two fresh rows with a different amount onto a
        # new canonical event; reconcile creates a second pending event.
        row_a2 = self.add_row(
            self.company_a, own_account="6222000000000001",
            cp_account="6222000000000002", expense="150.00",
            at="2026-02-01T10:00:00",
        )
        row_b2 = self.add_row(
            self.company_b, own_account="6222000000000002",
            cp_account="6222000000000001", income="150.00",
            at="2026-02-01T11:00:00",
        )
        current_row = self.connection.execute(
            "SELECT d.* FROM current_transfer_decisions c "
            "JOIN transfer_match_decisions d ON d.id = c.decision_id "
            "WHERE c.event_id = ?",
            (event_id,),
        ).fetchone()
        matching.apply_manual_decision(
            self.connection, event_id, "link_rows",
            reason="补录双边流水",
            expected_revision=current_row["revision"], request_key="link-1",
            actor=self.admin, source_row_ids=[row_a2, row_b2],
        )
        ledger_events.reconcile_bank_events(self.connection, actor=self.admin)
        amounts = sorted(
            row["amount"] for row in self.connection.execute(
                "SELECT amount FROM ledger_event_revisions WHERE state = 'pending_subject'"
            ).fetchall()
        )
        self.assertEqual(["100.00", "150.00"], amounts)

    def test_confirm_subject_enters_position_view(self) -> None:
        self.pair(self.company_a, self.company_b, "100.00")
        ledger_events.reconcile_bank_events(self.connection, actor=self.admin)
        event_id = self.ledger_events()[0]["id"]
        subjects.confirm_subject(
            self.connection, event_id,
            perspective_company_id=self.company_a, subject_code="other_receivable",
            reason="借款建议其他应收", expected_revision=1, request_key="k1",
            actor=self.admin,
        )
        positions = self.position_events()
        self.assertEqual(1, len(positions))
        self.assertEqual("other_receivable", positions[0]["subject_code"])
        self.assertEqual(self.company_a, positions[0]["perspective_company_id"])

    def test_confirm_stale_revision_conflicts(self) -> None:
        self.pair(self.company_a, self.company_b, "100.00")
        ledger_events.reconcile_bank_events(self.connection, actor=self.admin)
        event_id = self.ledger_events()[0]["id"]
        subjects.confirm_subject(
            self.connection, event_id,
            perspective_company_id=self.company_a, subject_code="receivable",
            reason="确认应收", expected_revision=1, request_key="k1",
            actor=self.admin,
        )
        with self.assertRaises(subjects.SubjectConflictError):
            subjects.confirm_subject(
                self.connection, event_id,
                perspective_company_id=self.company_a, subject_code="receivable",
                reason="重复确认", expected_revision=1, request_key="k2",
                actor=self.admin,
            )

    def test_reversal_mirrors_direction_and_subject(self) -> None:
        self.pair(self.company_a, self.company_b, "100.00")
        ledger_events.reconcile_bank_events(self.connection, actor=self.admin)
        event_id = self.ledger_events()[0]["id"]
        subjects.confirm_subject(
            self.connection, event_id,
            perspective_company_id=self.company_a, subject_code="other_receivable",
            reason="借款", expected_revision=1, request_key="k1", actor=self.admin,
        )
        reversal_id, _ = ledger_events.create_reversal(
            self.connection, event_id, source_kind="adjustment",
            reason="冲销错误确认", actor=self.admin,
        )
        reversal = self.current(reversal_id)
        self.assertEqual("reversal", reversal["posting_kind"])
        self.assertEqual(self.company_b, reversal["payer_company_id"])
        self.assertEqual(self.company_a, reversal["payee_company_id"])
        self.assertEqual("other_payable", reversal["subject_code"])
        self.assertEqual(self.company_b, reversal["perspective_company_id"])
        # Original event stays confirmed and still in the position view.
        self.assertEqual("confirmed", self.current(event_id)["state"])
        positions = self.position_events()
        self.assertEqual(2, len(positions))
        signed = sum(
            Decimal(position["amount"]) * (
                1 if position["payer_company_id"] == self.company_a else -1
            )
            for position in positions
        )
        self.assertEqual(Decimal("0"), signed)

    def test_evidence_rows_stay_immutable_under_reversal(self) -> None:
        self.pair(self.company_a, self.company_b, "100.00")
        ledger_events.reconcile_bank_events(self.connection, actor=self.admin)
        event_id = self.ledger_events()[0]["id"]
        subjects.confirm_subject(
            self.connection, event_id,
            perspective_company_id=self.company_a, subject_code="receivable",
            reason="确认", expected_revision=1, request_key="k1", actor=self.admin,
        )
        before = self.connection.execute(
            "SELECT income, expense FROM source_rows ORDER BY id"
        ).fetchall()
        ledger_events.create_reversal(
            self.connection, event_id, source_kind="adjustment",
            reason="冲销", actor=self.admin,
        )
        after = self.connection.execute(
            "SELECT income, expense FROM source_rows ORDER BY id"
        ).fetchall()
        self.assertEqual(before, after)

    def test_reversal_idempotent_guard(self) -> None:
        self.pair(self.company_a, self.company_b, "100.00")
        ledger_events.reconcile_bank_events(self.connection, actor=self.admin)
        event_id = self.ledger_events()[0]["id"]
        subjects.confirm_subject(
            self.connection, event_id,
            perspective_company_id=self.company_a, subject_code="receivable",
            reason="确认", expected_revision=1, request_key="k1", actor=self.admin,
        )
        # Simulate the B-43 event leaving the eligible set.
        with self.connection:
            self.connection.execute(
                "UPDATE canonical_transfer_events SET lifecycle = 'superseded' WHERE id = ?",
                (self.eligible()[0]["event_id"],),
            )
        stats = ledger_events.reconcile_bank_events(self.connection, actor=self.admin)
        self.assertEqual(1, stats["reversal"])
        reversals = self.connection.execute(
            "SELECT COUNT(*) AS n FROM ledger_event_revisions WHERE posting_kind = 'reversal'"
        ).fetchone()["n"]
        self.assertEqual(1, reversals)
        # Re-run does not create a second reversal.
        stats = ledger_events.reconcile_bank_events(self.connection, actor=self.admin)
        self.assertEqual(0, stats["reversal"])


class ProjectionTests(LedgerBase):
    def test_rebuild_current_projection(self) -> None:
        self.pair(self.company_a, self.company_b, "100.00")
        ledger_events.reconcile_bank_events(self.connection, actor=self.admin)
        event_id = self.ledger_events()[0]["id"]
        subjects.confirm_subject(
            self.connection, event_id,
            perspective_company_id=self.company_a, subject_code="receivable",
            reason="确认", expected_revision=1, request_key="k1", actor=self.admin,
        )
        self.connection.execute("DELETE FROM current_ledger_event_revisions")
        rebuilt = ledger_events.rebuild_current_ledger_projection(self.connection)
        self.assertGreaterEqual(rebuilt, 1)
        revision = self.current(event_id)
        self.assertEqual("confirmed", revision["state"])
        self.assertEqual("receivable", revision["subject_code"])


class SubjectSuggestionTests(LedgerBase):
    def test_mirror_mapping_is_symmetric(self) -> None:
        for subject, mirror in (
            ("receivable", "payable"), ("payable", "receivable"),
            ("other_receivable", "other_payable"), ("other_payable", "other_receivable"),
        ):
            self.assertEqual(mirror, subjects.mirror_subject(subject))
            self.assertEqual(subject, subjects.mirror_subject(mirror))

    def test_loan_keyword_suggests_payer_other_receivable(self) -> None:
        self.pair(self.company_a, self.company_b, "100.00", summary="借款", purpose="往来款")
        ledger_events.reconcile_bank_events(self.connection, actor=self.admin)
        event_id = self.ledger_events()[0]["id"]
        suggestions = subjects.compute_suggestions(self.connection, event_id)
        self.assertEqual(1, len(suggestions))
        self.assertEqual(self.company_a, suggestions[0]["suggested_perspective_company_id"])
        self.assertEqual("other_receivable", suggestions[0]["suggested_subject_code"])
        self.assertFalse(suggestions[0]["evidence"]["approved"])

    def test_repay_keyword_suggests_payee(self) -> None:
        self.pair(self.company_a, self.company_b, "100.00", summary="归还往来款", purpose="还款")
        ledger_events.reconcile_bank_events(self.connection, actor=self.admin)
        event_id = self.ledger_events()[0]["id"]
        suggestions = subjects.compute_suggestions(self.connection, event_id)
        self.assertEqual(1, len(suggestions))
        self.assertEqual(self.company_b, suggestions[0]["suggested_perspective_company_id"])

    def test_trade_keyword_never_suggests(self) -> None:
        # Trade vocabulary needs group approval; no dictionary hit -> review.
        self.pair(self.company_a, self.company_b, "100.00", summary="货款", purpose="采购货款")
        ledger_events.reconcile_bank_events(self.connection, actor=self.admin)
        event_id = self.ledger_events()[0]["id"]
        self.assertEqual([], subjects.compute_suggestions(self.connection, event_id))

    def test_name_alone_never_confirms(self) -> None:
        # A name match on the counterparty cannot decide the subject.
        self.pair(self.company_a, self.company_b, "100.00", summary="某客户往来", purpose=None)
        ledger_events.reconcile_bank_events(self.connection, actor=self.admin)
        event_id = self.ledger_events()[0]["id"]
        current = self.current(event_id)
        self.assertEqual("pending_subject", current["state"])
        self.assertEqual([], subjects.compute_suggestions(self.connection, event_id))


if __name__ == "__main__":
    unittest.main()
