"""Dashboard aggregate helpers and admin HTTP endpoints (no openpyxl dependency)."""

from __future__ import annotations

import os
from pathlib import Path
import tempfile
import threading
import unittest

from bank_importer import dashboard, master_data
from bank_importer.db import connect, migrate

import server
from test_server_auth import Client, as_json

BOOTSTRAP_PASSWORD = "BootAdmin123"
ADMIN_PASSWORD = "AdminPass123"


class DashboardUnitTests(unittest.TestCase):
    def setUp(self) -> None:
        self.connection = connect(":memory:")
        migrate(self.connection)

    def tearDown(self) -> None:
        self.connection.close()

    def test_empty_dashboard(self) -> None:
        payload = dashboard.build_dashboard(
            self.connection, from_date="2026-01-01", cutoff="2026-08-20"
        )
        self.assertEqual(0, payload["audit"]["total"])
        self.assertEqual([], payload["companies"])
        self.assertEqual(7, len(payload["weekly_flow"]["labels"]))

    def test_pending_account_counts_as_medium(self) -> None:
        now = master_data.utc_now()
        cursor = self.connection.execute(
            "INSERT INTO companies (name, credit_code, cashier_name, status, created_at, updated_at) "
            "VALUES ('甲公司', NULL, NULL, 'active', ?, ?)",
            (now, now),
        )
        company_id = int(cursor.lastrowid)
        self.connection.commit()
        master_data.submit_bank_account(
            self.connection,
            company_id=company_id,
            bank_name="工行",
            account_type="一般户",
            account_number="6222020000000001",
            start_date="2026-01-01",
            actor=None,
        )
        counts = dashboard.audit_counts(self.connection)
        self.assertEqual(1, counts["medium"])
        self.assertEqual(0, counts["high"])
        self.assertEqual(1, counts["total"])

    def test_pending_manual_counts_as_medium(self) -> None:
        from bank_importer import auth, manual_records

        now = master_data.utc_now()
        a = self.connection.execute(
            "INSERT INTO companies (name, credit_code, cashier_name, status, created_at, updated_at) "
            "VALUES ('甲公司', NULL, NULL, 'active', ?, ?)",
            (now, now),
        ).lastrowid
        b = self.connection.execute(
            "INSERT INTO companies (name, credit_code, cashier_name, status, created_at, updated_at) "
            "VALUES ('乙公司', NULL, NULL, 'active', ?, ?)",
            (now, now),
        ).lastrowid
        self.connection.commit()
        auth.create_user(
            self.connection, "cashier-a", "CashierPass123", "company", company_id=int(a)
        )
        actor = self.connection.execute(
            "SELECT * FROM users WHERE username = 'cashier-a'"
        ).fetchone()
        manual_records.submit(
            self.connection,
            company_id=int(a),
            counterparty_company_id=int(b),
            occurred_at="2026-02-01T09:00:00",
            direction="incoming",
            amount="100.00",
            currency="CNY",
            funding_source="other",
            requested_subject="receivable",
            request_key="hel157-manual-1",
            actor=actor,
        )
        counts = dashboard.audit_counts(self.connection)
        self.assertEqual(1, counts["medium"])
        self.assertEqual(0, counts["high"])
        self.assertEqual(1, counts["total"])

    def test_audit_total_equals_queue_sum(self) -> None:
        """Homepage / badge / audit heading must share one backend total."""
        now = master_data.utc_now()
        a = self.connection.execute(
            "INSERT INTO companies (name, credit_code, cashier_name, status, created_at, updated_at) "
            "VALUES ('甲公司', NULL, NULL, 'active', ?, ?)",
            (now, now),
        ).lastrowid
        self.connection.commit()
        master_data.submit_bank_account(
            self.connection,
            company_id=int(a),
            bank_name="工行",
            account_type="一般户",
            account_number="6222020000000002",
            start_date="2026-01-01",
            actor=None,
        )
        event_id = self.connection.execute(
            "INSERT INTO canonical_transfer_events (lifecycle, created_at) VALUES ('active', ?)",
            (now,),
        ).lastrowid
        decision_id = self.connection.execute(
            """
            INSERT INTO transfer_match_decisions (
              event_id, revision, effective_at, amount, currency, classification,
              pairing, locked, mode, rule_version, created_at
            ) VALUES (?, 1, '2026-07-05T10:00:00', '100000.00', 'CNY', 'unresolved',
                      'single', 0, 'manual', 'test', ?)
            """,
            (event_id, now),
        ).lastrowid
        self.connection.execute(
            "INSERT INTO current_transfer_decisions (event_id, decision_id) VALUES (?, ?)",
            (event_id, decision_id),
        )
        self.connection.commit()

        counts = dashboard.audit_counts(self.connection)
        payload = dashboard.build_dashboard(
            self.connection, from_date="2026-01-01", cutoff="2026-08-20"
        )
        self.assertEqual(counts, payload["audit"])
        self.assertEqual(
            counts["total"],
            counts["high"] + counts["medium"] + counts["low"],
        )
        self.assertEqual(1, counts["high"])
        self.assertEqual(1, counts["medium"])
        self.assertEqual(2, counts["total"])

    def test_company_summaries_from_eligible_events(self) -> None:
        now = master_data.utc_now()
        a = self.connection.execute(
            "INSERT INTO companies (name, credit_code, cashier_name, status, created_at, updated_at) "
            "VALUES ('甲公司', NULL, NULL, 'active', ?, ?)",
            (now, now),
        ).lastrowid
        b = self.connection.execute(
            "INSERT INTO companies (name, credit_code, cashier_name, status, created_at, updated_at) "
            "VALUES ('乙公司', NULL, NULL, 'active', ?, ?)",
            (now, now),
        ).lastrowid
        event_id = self.connection.execute(
            "INSERT INTO canonical_transfer_events (lifecycle, created_at) VALUES ('active', ?)",
            (now,),
        ).lastrowid
        decision_id = self.connection.execute(
            """
            INSERT INTO transfer_match_decisions (
              event_id, revision, effective_at, amount, currency, classification,
              pairing, locked, mode, rule_version, created_at
            ) VALUES (?, 1, '2026-07-05T10:00:00', '100000.00', 'CNY', 'intercompany',
                      'paired', 0, 'manual', 'test', ?)
            """,
            (event_id, now),
        ).lastrowid
        self.connection.execute(
            "INSERT INTO current_transfer_decisions (event_id, decision_id) VALUES (?, ?)",
            (event_id, decision_id),
        )
        self.connection.execute(
            """
            INSERT INTO transfer_decision_participants (
              decision_id, role, company_id, bank_account_id, resolve_method, created_at
            ) VALUES (?, 'payer', ?, NULL, 'manual', ?), (?, 'payee', ?, NULL, 'manual', ?)
            """,
            (decision_id, a, now, decision_id, b, now),
        )
        self.connection.commit()

        items, totals = dashboard.company_summaries(
            self.connection, from_date="2026-01-01", cutoff="2026-08-20"
        )
        self.assertEqual(1, totals["detail_count"])
        by_name = {row["name"]: row for row in items}
        self.assertEqual(1, by_name["甲公司"]["detail_count"])
        self.assertEqual("10.00", by_name["甲公司"]["debit_wan"])
        self.assertEqual("-10.00", by_name["甲公司"]["net_wan"])
        self.assertEqual("10.00", by_name["乙公司"]["credit_wan"])
        self.assertEqual("10.00", by_name["乙公司"]["net_wan"])

        detail = dashboard.company_peer_groups(
            self.connection, int(a), from_date="2026-01-01", cutoff="2026-08-20"
        )
        self.assertEqual(1, len(detail["groups"]))
        self.assertEqual("乙公司", detail["groups"][0]["peer_name"])
        self.assertIsNone(detail["groups"][0]["opening"])


class DashboardApiTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        cls.temp_dir = tempfile.TemporaryDirectory()
        root = Path(cls.temp_dir.name)
        cls.db_path = root / "app.db"
        cls.storage = root / "files"

        cls._old_db_path = server.DB_PATH
        cls._old_storage = server.STORAGE_DIR
        server.DB_PATH = cls.db_path
        server.STORAGE_DIR = cls.storage

        os.environ["APP_BOOTSTRAP_ADMIN_PASSWORD"] = BOOTSTRAP_PASSWORD
        connection = connect(cls.db_path)
        migrate(connection)
        assert server.ensure_bootstrap_admin(connection) is None
        connection.close()

        class QuietHandler(server.AppHandler):
            def log_message(self, *args) -> None:
                pass

        cls.httpd = server.ThreadingHTTPServer(("127.0.0.1", 0), QuietHandler)
        cls.port = cls.httpd.server_address[1]
        cls.thread = threading.Thread(target=cls.httpd.serve_forever, daemon=True)
        cls.thread.start()

        cls.admin = Client("127.0.0.1", cls.port)
        status, _, data = cls.admin.post_json(
            "/api/login",
            {"username": "group-admin", "password": BOOTSTRAP_PASSWORD, "portal": "admin"},
        )
        assert status == 200, data
        status, _, data = cls.admin.post_json(
            "/api/password/change",
            {"old_password": BOOTSTRAP_PASSWORD, "new_password": ADMIN_PASSWORD},
        )
        assert status == 200, data

        status, _, data = cls.admin.post_json(
            "/api/admin/companies", {"name": "甲公司", "username": "cashier-a"}
        )
        assert status == 200, data

    @classmethod
    def tearDownClass(cls) -> None:
        cls.httpd.shutdown()
        cls.httpd.server_close()
        server.DB_PATH = cls._old_db_path
        server.STORAGE_DIR = cls._old_storage
        cls.temp_dir.cleanup()

    def test_dashboard_ok_for_admin(self) -> None:
        status, _, raw = self.admin.get(
            "/api/admin/dashboard?from=2026-01-01&cutoff=2026-08-20"
        )
        data = as_json(raw)
        self.assertEqual(200, status)
        self.assertEqual("ok", data["status"])
        self.assertIn("audit", data)
        self.assertEqual(1, len(data["companies"]))
        self.assertEqual("甲公司", data["companies"][0]["name"])

    def test_audit_counts_sync_after_account_review(self) -> None:
        """Pending account raises dashboard.audit; approve brings total back down."""
        from bank_importer.db import connect as db_connect
        from bank_importer import master_data as md

        status, _, raw = self.admin.get("/api/admin/companies")
        company_id = as_json(raw)["companies"][0]["id"]

        connection = db_connect(self.db_path)
        try:
            account = md.submit_bank_account(
                connection,
                company_id=company_id,
                bank_name="工行",
                account_type="一般户",
                account_number="6222020000000099",
                start_date="2026-01-01",
                actor=None,
            )
            account_id = account["id"]
        finally:
            connection.close()

        status, _, raw = self.admin.get("/api/admin/dashboard")
        before = as_json(raw)["audit"]
        self.assertEqual(200, status)
        self.assertGreaterEqual(before["medium"], 1)
        self.assertGreaterEqual(before["total"], 1)

        status, _, raw = self.admin.post_json(
            f"/api/admin/accounts/{account_id}/review",
            {"decision": "approve", "reason": "HEL-157 sync test"},
        )
        self.assertEqual(200, status, raw)

        status, _, raw = self.admin.get("/api/admin/dashboard")
        after = as_json(raw)["audit"]
        self.assertEqual(after["medium"], before["medium"] - 1)
        self.assertEqual(after["total"], before["total"] - 1)
        self.assertEqual(
            after["total"], after["high"] + after["medium"] + after["low"]
        )

    def test_three_queue_audit_parity_and_dispose_sync(self) -> None:
        """Seed account+manual+exception: list sizes == dashboard.audit; dispose syncs -1."""
        from bank_importer.db import connect as db_connect
        from bank_importer import auth, manual_records, master_data as md

        status, _, raw = self.admin.get("/api/admin/companies")
        company_id = as_json(raw)["companies"][0]["id"]

        # Second company for manual counterparty.
        status, _, raw = self.admin.post_json(
            "/api/admin/companies", {"name": "乙公司", "username": "cashier-b-hel157"}
        )
        self.assertEqual(200, status, raw)
        company_b = as_json(raw).get("company_id") or as_json(raw).get("id")
        self.assertIsNotNone(company_b)

        connection = db_connect(self.db_path)
        try:
            account = md.submit_bank_account(
                connection,
                company_id=company_id,
                bank_name="工行",
                account_type="一般户",
                account_number="6222020000000157",
                start_date="2026-01-01",
                actor=None,
            )
            account_id = account["id"]

            cashier = connection.execute(
                "SELECT * FROM users WHERE username = 'cashier-a'"
            ).fetchone()
            if cashier is None:
                auth.create_user(
                    connection,
                    "cashier-a",
                    "CashierPass123",
                    "company",
                    company_id=company_id,
                )
                cashier = connection.execute(
                    "SELECT * FROM users WHERE username = 'cashier-a'"
                ).fetchone()
            manual = manual_records.submit(
                connection,
                company_id=company_id,
                counterparty_company_id=int(company_b),
                occurred_at="2026-02-01T09:00:00",
                direction="incoming",
                amount="100.00",
                currency="CNY",
                funding_source="other",
                requested_subject="receivable",
                request_key="hel157-queue-manual",
                actor=cashier,
            )

            now = md.utc_now()
            event_id = connection.execute(
                "INSERT INTO canonical_transfer_events (lifecycle, created_at) VALUES ('active', ?)",
                (now,),
            ).lastrowid
            decision_id = connection.execute(
                """
                INSERT INTO transfer_match_decisions (
                  event_id, revision, effective_at, amount, currency, classification,
                  pairing, locked, mode, rule_version, created_at
                ) VALUES (?, 1, '2026-07-05T10:00:00', '100000.00', 'CNY', 'unresolved',
                          'single', 0, 'manual', 'test', ?)
                """,
                (event_id, now),
            ).lastrowid
            connection.execute(
                "INSERT INTO current_transfer_decisions (event_id, decision_id) VALUES (?, ?)",
                (event_id, decision_id),
            )
            connection.commit()
            manual_id = manual["id"]
            manual_decision_id = manual["decision_id"]
        finally:
            connection.close()

        def queue_sizes():
            st, _, body = self.admin.get("/api/admin/accounts?status=pending")
            self.assertEqual(200, st, body)
            accounts_n = len(as_json(body)["accounts"])
            st, _, body = self.admin.get("/api/admin/manual-records?state=pending")
            self.assertEqual(200, st, body)
            manuals_n = len(as_json(body)["records"])
            st, _, body = self.admin.get("/api/admin/match-exceptions")
            self.assertEqual(200, st, body)
            exceptions_n = len(as_json(body)["exceptions"])
            return accounts_n, manuals_n, exceptions_n, accounts_n + manuals_n + exceptions_n

        def assert_parity(expected_total: int) -> dict:
            st, _, body = self.admin.get("/api/admin/dashboard")
            self.assertEqual(200, st, body)
            audit = as_json(body)["audit"]
            accounts_n, manuals_n, exceptions_n, list_total = queue_sizes()
            self.assertEqual(expected_total, audit["total"])
            self.assertEqual(expected_total, list_total)
            self.assertEqual(
                audit["total"], audit["high"] + audit["medium"] + audit["low"]
            )
            self.assertGreaterEqual(accounts_n, 1 if expected_total >= 3 else 0)
            return {
                "audit": audit,
                "accounts": accounts_n,
                "manuals": manuals_n,
                "exceptions": exceptions_n,
            }

        before = assert_parity(3)
        self.assertEqual(1, before["accounts"])
        self.assertEqual(1, before["manuals"])
        self.assertEqual(1, before["exceptions"])
        self.assertEqual(1, before["audit"]["high"])
        self.assertEqual(2, before["audit"]["medium"])

        # Dispose account → total 2
        st, _, body = self.admin.post_json(
            f"/api/admin/accounts/{account_id}/review",
            {"decision": "approve", "reason": "HEL-157 three-queue dispose account"},
        )
        self.assertEqual(200, st, body)
        after_account = assert_parity(2)

        # Dispose manual → total 1
        st, _, body = self.admin.post_json(
            f"/api/admin/manual-records/{manual_id}/decisions",
            {
                "action": "approve_new",
                "reason": "HEL-157 three-queue dispose manual",
                "expected_decision_id": manual_decision_id,
                "request_key": "hel157-dispose-manual",
            },
        )
        self.assertEqual(200, st, body)
        after_manual = assert_parity(1)
        self.assertEqual(0, after_manual["manuals"])

        # Dispose match exception via reverse → total 0
        st, _, body = self.admin.get("/api/admin/match-exceptions")
        exceptions = as_json(body)["exceptions"]
        self.assertEqual(1, len(exceptions))
        target = exceptions[0]
        st, _, body = self.admin.post_json(
            f"/api/admin/transfer-events/{target['event_id']}/decisions",
            {
                "action": "reverse",
                "reason": "HEL-157 three-queue dispose match",
                "expected_revision": target["revision"],
                "request_key": "hel157-dispose-match",
            },
        )
        self.assertEqual(200, st, body)
        after_match = assert_parity(0)
        self.assertEqual(0, after_match["exceptions"])
        self.assertEqual(after_account["audit"]["total"] - 1, after_manual["audit"]["total"])
        self.assertEqual(after_manual["audit"]["total"] - 1, after_match["audit"]["total"])

    def test_company_detail_missing(self) -> None:
        status, _, raw = self.admin.get(
            "/api/admin/dashboard/companies/999999?from=2026-01-01&cutoff=2026-08-20"
        )
        data = as_json(raw)
        self.assertEqual(404, status)
        self.assertEqual("error", data["status"])

    def test_company_detail_ok(self) -> None:
        status, _, raw = self.admin.get("/api/admin/dashboard")
        data = as_json(raw)
        company_id = data["companies"][0]["id"]
        status, _, raw = self.admin.get(
            f"/api/admin/dashboard/companies/{company_id}?from=2026-01-01&cutoff=2026-08-20"
        )
        detail = as_json(raw)
        self.assertEqual(200, status)
        self.assertEqual([], detail["groups"])


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