"""HEL-206: 公司间期末余额方向——期初应收 + 转出垫付 = 期末应收增加。"""

from __future__ import annotations

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

from bank_importer import auth, calculation, matching, master_data
from bank_importer.db import connect, migrate, utc_now

import server

ROOT = Path(__file__).resolve().parents[1]
ADMIN_PASSWORD = "AdminPass123"
CASHIER_A_PASSWORD = "CashierA123"
CASHIER_B_PASSWORD = "CashierB123"

try:
    from playwright.sync_api import sync_playwright
except ImportError:  # pragma: no cover
    sync_playwright = None


def _prepare_chrome_libs() -> Path | None:
    candidates = [ROOT / ".chrome-libs" / "lib"]
    for lib_dir in candidates:
        if (lib_dir / "libatk-1.0.so.0").exists():
            current = os.environ.get("LD_LIBRARY_PATH", "")
            prefix = str(lib_dir)
            if prefix not in current.split(":"):
                os.environ["LD_LIBRARY_PATH"] = (
                    f"{prefix}:{current}" if current else prefix
                )
            return lib_dir
    return None


def _chromium_available() -> bool:
    if not sync_playwright:
        return False
    _prepare_chrome_libs()
    try:
        with sync_playwright() as p:
            browser = p.chromium.launch(headless=True, args=["--no-sandbox"])
            browser.close()
        return True
    except Exception:
        return False


class Hel206BalanceDirectionUnitTests(unittest.TestCase):
    """不依赖浏览器：期初 200 + 转出 100 → 甲应收 300 / 乙应付 300。"""

    def setUp(self) -> None:
        self.temp_dir = tempfile.TemporaryDirectory()
        self.addCleanup(self.temp_dir.cleanup)
        self.db_path = Path(self.temp_dir.name) / "app.db"
        self.connection = connect(self.db_path)
        self.addCleanup(self.connection.close)
        migrate(self.connection)
        auth.create_user(self.connection, "admin-u", ADMIN_PASSWORD, "admin")
        self.admin = self.connection.execute(
            "SELECT * FROM users WHERE username = 'admin-u'"
        ).fetchone()
        self.company_a = master_data.create_company(
            self.connection, "甲公司", None, None, None
        )
        self.company_b = master_data.create_company(
            self.connection, "乙公司", None, None, None
        )
        self.account_a = self._approve("6222000000000001", self.company_a)
        self.account_b = self._approve("6222000000000002", self.company_b)
        calculation.set_calculation_start_date(
            self.connection, "2026-01-01", "起算", self.admin
        )

    def _approve(self, number: str, company_id: int):
        account = master_data.submit_bank_account(
            self.connection,
            company_id=company_id,
            bank_name="中信银行",
            account_type="基本户",
            account_number=number,
            start_date="2026-01-01",
            actor=None,
        )
        return master_data.review_bank_account(
            self.connection,
            account["id"],
            "approve",
            None,
            self.admin,
            effective_from="2026-01-01",
        )

    def _add_row(self, company_id, account_id, own, cp, *, income, expense, at, ref):
        with self.connection:
            cursor = self.connection.execute(
                """
                INSERT INTO source_files
                  (sha256, original_filename, size_bytes, storage_path, created_at)
                VALUES (?, 'xfer.xlsx', 1, 'data/files/xfer.xlsx', ?)
                """,
                (ref, utc_now()),
            )
            source_file_id = int(cursor.lastrowid)
            cursor = self.connection.execute(
                """
                INSERT INTO import_batches (
                    source_file_id, status, company_id, upload_bank_account_id,
                    created_at, updated_at
                ) VALUES (?, 'parsed', ?, ?, ?, ?)
                """,
                (source_file_id, company_id, account_id, utc_now(), utc_now()),
            )
            batch_id = int(cursor.lastrowid)
            cursor = self.connection.execute(
                """
                INSERT INTO sheet_batches (
                    import_batch_id, sheet_name, bank_name, template_id,
                    template_version, header_row, transaction_count, warnings,
                    created_at
                ) VALUES (?, '流水', '测试银行', 'test-v1', 1, 1, 1, '[]', ?)
                """,
                (batch_id, utc_now()),
            )
            sheet_batch_id = int(cursor.lastrowid)
            self.connection.execute(
                """
                INSERT INTO sheet_reviews (
                    import_batch_id, sheet_name, outcome, sheet_batch_id,
                    review_status, created_at
                ) VALUES (?, '流水', 'parsed', ?, 'confirmed', ?)
                """,
                (batch_id, sheet_batch_id, utc_now()),
            )
            cursor = self.connection.execute(
                """
                INSERT INTO source_rows (
                    sheet_batch_id, source_row, transaction_at, income, expense,
                    own_account, own_name, counterparty_account, counterparty_name,
                    summary, purpose, currency, created_at
                ) VALUES (?, 1, ?, ?, ?, ?, '测试', ?, '对方', '往来', '往来款', 'CNY', ?)
                """,
                (sheet_batch_id, at, income, expense, own, cp, utc_now()),
            )
        return int(cursor.lastrowid)

    def test_opening_200_plus_outflow_100_equals_closing_300(self) -> None:
        item = calculation.create_opening_balance(
            self.connection, self.company_a, self.company_b, "200", "期初应收", self.admin
        )
        calculation.confirm_opening_balance(
            self.connection, item["id"], "确认", self.admin
        )
        row_a = self._add_row(
            self.company_a,
            self.account_a["id"],
            "6222000000000001",
            "6222000000000002",
            income="0",
            expense="100.00",
            at="2026-01-15T10:00:00",
            ref="sha-hel206-a",
        )
        row_b = self._add_row(
            self.company_b,
            self.account_b["id"],
            "6222000000000002",
            "6222000000000001",
            income="100.00",
            expense="0",
            at="2026-01-15T11:00:00",
            ref="sha-hel206-b",
        )
        matching.reconcile_rows(self.connection, [row_a, row_b])

        bal_a = calculation.compute_pair_balance(
            self.connection, self.company_a, self.company_b, cutoff="2026-01-31"
        )
        bal_b = calculation.compute_pair_balance(
            self.connection, self.company_b, self.company_a, cutoff="2026-01-31"
        )
        self.assertEqual("200", bal_a["opening"])
        self.assertEqual("100.00", bal_a["net_change"])
        self.assertEqual("300.00", bal_a["closing"])
        self.assertEqual("-200", bal_b["opening"])
        self.assertEqual("-100.00", bal_b["net_change"])
        self.assertEqual("-300.00", bal_b["closing"])
        self.assertEqual(
            Decimal(bal_a["closing"]) + Decimal(bal_b["closing"]),
            Decimal("0"),
        )


@unittest.skipUnless(sync_playwright, "playwright 未安装，跳过浏览器冒烟")
@unittest.skipUnless(_chromium_available(), "chromium 无法启动，跳过浏览器冒烟")
class Hel206BrowserDirectionTests(unittest.TestCase):
    """真实 Chromium：公司端转账往来页期末方向与对手公司对称。"""

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

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

        connection = connect(cls.db_path)
        migrate(connection)
        auth.create_user(
            connection, "group-admin", ADMIN_PASSWORD, "admin",
            must_change_password=False,
        )
        cls.company_a = master_data.create_company(
            connection, "甲公司", None, None, None
        )
        cls.company_b = master_data.create_company(
            connection, "乙公司", None, None, None
        )
        auth.create_user(
            connection, "cashier-a", CASHIER_A_PASSWORD, "company",
            cls.company_a, must_change_password=False,
        )
        auth.create_user(
            connection, "cashier-b", CASHIER_B_PASSWORD, "company",
            cls.company_b, must_change_password=False,
        )
        admin = connection.execute(
            "SELECT * FROM users WHERE username = 'group-admin'"
        ).fetchone()

        def approve(company_id, number):
            account = master_data.submit_bank_account(
                connection,
                company_id=company_id,
                bank_name="中信银行",
                account_type="基本户",
                account_number=number,
                start_date="2026-01-01",
                actor=None,
            )
            return master_data.review_bank_account(
                connection, account["id"], "approve", None, admin,
                effective_from="2026-01-01",
            )

        account_a = approve(cls.company_a, "6222000000000001")
        account_b = approve(cls.company_b, "6222000000000002")
        calculation.set_calculation_start_date(
            connection, "2026-01-01", "初始化起算", admin
        )
        item = calculation.create_opening_balance(
            connection, cls.company_a, cls.company_b, "200", "期初应收", admin
        )
        calculation.confirm_opening_balance(connection, item["id"], "确认", admin)

        def add_row(company_id, account_id, own, cp, *, income, expense, at, ref):
            with connection:
                cursor = connection.execute(
                    """
                    INSERT INTO source_files
                      (sha256, original_filename, size_bytes, storage_path, created_at)
                    VALUES (?, 'xfer.xlsx', 1, 'data/files/xfer.xlsx', ?)
                    """,
                    (ref, utc_now()),
                )
                source_file_id = int(cursor.lastrowid)
                cursor = connection.execute(
                    """
                    INSERT INTO import_batches (
                        source_file_id, status, company_id, upload_bank_account_id,
                        created_at, updated_at
                    ) VALUES (?, 'parsed', ?, ?, ?, ?)
                    """,
                    (source_file_id, company_id, account_id, utc_now(), utc_now()),
                )
                batch_id = int(cursor.lastrowid)
                cursor = connection.execute(
                    """
                    INSERT INTO sheet_batches (
                        import_batch_id, sheet_name, bank_name, template_id,
                        template_version, header_row, transaction_count, warnings,
                        created_at
                    ) VALUES (?, '流水', '测试银行', 'test-v1', 1, 1, 1, '[]', ?)
                    """,
                    (batch_id, utc_now()),
                )
                sheet_batch_id = int(cursor.lastrowid)
                connection.execute(
                    """
                    INSERT INTO sheet_reviews (
                        import_batch_id, sheet_name, outcome, sheet_batch_id,
                        review_status, created_at
                    ) VALUES (?, '流水', 'parsed', ?, 'confirmed', ?)
                    """,
                    (batch_id, sheet_batch_id, utc_now()),
                )
                cursor = connection.execute(
                    """
                    INSERT INTO source_rows (
                        sheet_batch_id, source_row, transaction_at, income, expense,
                        own_account, own_name, counterparty_account, counterparty_name,
                        summary, purpose, currency, created_at
                    ) VALUES (?, 1, ?, ?, ?, ?, '测试', ?, '对方', '往来', '往来款', 'CNY', ?)
                    """,
                    (sheet_batch_id, at, income, expense, own, cp, utc_now()),
                )
            return int(cursor.lastrowid)

        row_a = add_row(
            cls.company_a, account_a["id"], "6222000000000001", "6222000000000002",
            income="0", expense="100.00", at="2026-01-15T10:00:00", ref="sha-hel206-br-a",
        )
        row_b = add_row(
            cls.company_b, account_b["id"], "6222000000000002", "6222000000000001",
            income="100.00", expense="0", at="2026-01-15T11:00:00", ref="sha-hel206-br-b",
        )
        matching.reconcile_rows(connection, [row_a, row_b])
        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.base = f"http://127.0.0.1:{cls.port}"

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

    def _login(self, page, *, username: str, password: str) -> None:
        page.goto(f"{self.base}/login-company.html", wait_until="domcontentloaded")
        page.fill("#account", username)
        page.fill("#password", password)
        page.click('button[type="submit"]')
        page.wait_for_url("**/company.html", timeout=15000)

    def test_company_ending_direction_in_browser(self) -> None:
        with sync_playwright() as p:
            browser = p.chromium.launch(
                headless=True, args=["--no-sandbox", "--disable-dev-shm-usage"]
            )
            try:
                context = browser.new_context(viewport={"width": 1440, "height": 900})
                page = context.new_page()
                errors: list[str] = []
                page.on("pageerror", lambda err: errors.append(str(err)))

                # 甲公司：期初 200 + 转出 100 → 期末应收 300
                self._login(page, username="cashier-a", password=CASHIER_A_PASSWORD)
                page.click('a[data-view="transfers"]')
                page.wait_for_selector("#tfStatEndingCard:not([hidden])", timeout=15000)
                bal_a = page.evaluate(
                    """async () => {
                      const res = await fetch('/api/company/balances?cutoff=2026-01-31');
                      return await res.json();
                    }"""
                )
                self.assertEqual("ok", bal_a.get("status"))
                self.assertEqual("full", bal_a.get("basis"))
                pair_a = next(
                    p for p in bal_a["pairs"]
                    if p["counterparty_company_id"] == self.company_b
                )
                self.assertEqual("200", pair_a["opening"])
                self.assertEqual("100.00", pair_a["net_change"])
                self.assertEqual("300.00", pair_a["closing"])
                ending_text = page.locator("#tfStatEnding").inner_text()
                self.assertIn("0.03", ending_text)  # 300 元 = 0.03 万元
                self.assertEqual([], errors, errors)

                # 乙公司：对称应付 300
                self._login(page, username="cashier-b", password=CASHIER_B_PASSWORD)
                page.click('a[data-view="transfers"]')
                page.wait_for_selector("#tfStatEndingCard:not([hidden])", timeout=15000)
                bal_b = page.evaluate(
                    """async () => {
                      const res = await fetch('/api/company/balances?cutoff=2026-01-31');
                      return await res.json();
                    }"""
                )
                pair_b = next(
                    p for p in bal_b["pairs"]
                    if p["counterparty_company_id"] == self.company_a
                )
                self.assertEqual("-200", pair_b["opening"])
                self.assertEqual("-100.00", pair_b["net_change"])
                self.assertEqual("-300.00", pair_b["closing"])
                ending_b = page.locator("#tfStatEnding").inner_text()
                self.assertIn("0.03", ending_b)
                fatal = [e for e in errors if "is not defined" in e or "Cannot read" in e]
                self.assertEqual([], fatal, fatal)
            finally:
                browser.close()


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