"""Tests for automatic reminder detection, delivery, isolation and audit trail."""

from __future__ import annotations

from datetime import date, timedelta
import json
import unittest

from bank_importer import auth, reminders
from bank_importer.db import connect, migrate, utc_now


class ReminderTestCase(unittest.TestCase):
    def setUp(self) -> None:
        self.connection = connect(":memory:")
        self.addCleanup(self.connection.close)
        migrate(self.connection)
        now = utc_now()
        with self.connection:
            self.connection.execute(
                "INSERT INTO companies (name, created_at, updated_at) VALUES ('甲公司', ?, ?)",
                (now, now),
            )
            self.connection.execute(
                "INSERT INTO companies (name, created_at, updated_at) VALUES ('乙公司', ?, ?)",
                (now, now),
            )
        self.company_a = int(
            self.connection.execute("SELECT id FROM companies WHERE name = '甲公司'").fetchone()["id"]
        )
        self.company_b = int(
            self.connection.execute("SELECT id FROM companies WHERE name = '乙公司'").fetchone()["id"]
        )
        self.admin_id = auth.create_user(self.connection, "admin1", "AdminPass123", "admin")
        self.user_a = auth.create_user(
            self.connection, "cashier-a", "CashierA123", "company", company_id=self.company_a
        )
        self.user_b = auth.create_user(
            self.connection, "cashier-b", "CashierB123", "company", company_id=self.company_b
        )
        self.admin = self.connection.execute(
            "SELECT * FROM users WHERE id = ?", (self.admin_id,)
        ).fetchone()
        self.actor_a = self.connection.execute(
            "SELECT * FROM users WHERE id = ?", (self.user_a,)
        ).fetchone()

    def _insert_batch(
        self,
        company_id: int,
        *,
        period_start: str,
        period_end: str,
    ) -> None:
        now = utc_now()
        with self.connection:
            self.connection.execute(
                """
                INSERT INTO source_files (sha256, original_filename, size_bytes, storage_path, created_at)
                VALUES (?, 'f.xls', 1, '/tmp/f.xls', ?)
                """,
                (f"sha-{company_id}-{period_end}", now),
            )
            file_id = int(self.connection.execute("SELECT last_insert_rowid()").fetchone()[0])
            self.connection.execute(
                """
                INSERT INTO import_batches (source_file_id, status, company_id, created_at, updated_at)
                VALUES (?, 'parsed', ?, ?, ?)
                """,
                (file_id, company_id, now, now),
            )
            batch_id = int(self.connection.execute("SELECT last_insert_rowid()").fetchone()[0])
            self.connection.execute(
                """
                INSERT INTO sheet_batches (
                    import_batch_id, sheet_name, bank_name, template_id, template_version,
                    header_row, period_start, period_end, transaction_count, created_at
                ) VALUES (?, 's1', '工行', 'tpl', 1, 1, ?, ?, 1, ?)
                """,
                (batch_id, period_start, period_end, now),
            )

    def _insert_pending_account(self, company_id: int) -> None:
        now = utc_now()
        with self.connection:
            self.connection.execute(
                """
                INSERT INTO bank_accounts (
                    company_id, account_number, bank_name, status, created_at, updated_at
                ) VALUES (?, ?, '工行', 'pending', ?, ?)
                """,
                (company_id, f"622{company_id:012d}", now, now),
            )


class RuleDetectionTests(ReminderTestCase):
    def test_unsubmitted_detected_when_no_current_month_batch(self) -> None:
        today = date.today()
        if today.day < 5:
            self.skipTest("monthly start day gate not reached today")
        findings = reminders.scan_findings(self.connection)
        keys = {item.company_id: item for item in findings if item.rule_key == reminders.RULE_UNSUBMITTED}
        self.assertIn(self.company_a, keys)
        self.assertIn(self.company_b, keys)

    def test_unsubmitted_not_reported_when_batch_exists(self) -> None:
        today = date.today()
        if today.day < 5:
            self.skipTest("monthly start day gate not reached today")
        period = f"{today.year:04d}-{today.month:02d}"
        self._insert_batch(self.company_a, period_start=f"{period}-01", period_end=f"{period}-15")
        findings = reminders.scan_findings(self.connection)
        for item in findings:
            if item.rule_key == reminders.RULE_UNSUBMITTED:
                self.assertNotEqual(self.company_a, item.company_id)

    def test_gap_detected_after_threshold(self) -> None:
        old_end = (date.today() - timedelta(days=10)).isoformat()
        self._insert_batch(self.company_a, period_start="2026-01-01", period_end=old_end)
        findings = reminders.scan_findings(self.connection)
        gap = [item for item in findings if item.rule_key == reminders.RULE_GAP and item.company_id == self.company_a]
        self.assertEqual(1, len(gap))

    def test_gap_not_reported_for_recent_batch(self) -> None:
        recent = (date.today() - timedelta(days=1)).isoformat()
        self._insert_batch(self.company_a, period_start="2026-07-01", period_end=recent)
        findings = reminders.scan_findings(self.connection)
        gap = [item for item in findings if item.rule_key == reminders.RULE_GAP and item.company_id == self.company_a]
        self.assertEqual(0, len(gap))

    def test_pending_review_detected_with_pending_account(self) -> None:
        self._insert_pending_account(self.company_a)
        findings = reminders.scan_findings(self.connection)
        pending = [
            item for item in findings
            if item.rule_key == reminders.RULE_PENDING and item.company_id == self.company_a
        ]
        self.assertEqual(1, len(pending))
        self.assertGreater(pending[0].rule_params["pending_count"], 0)

    def test_pending_review_not_reported_when_clean(self) -> None:
        findings = reminders.scan_findings(self.connection)
        pending = [
            item for item in findings
            if item.rule_key == reminders.RULE_PENDING and item.company_id == self.company_a
        ]
        self.assertEqual(0, len(pending))


class DeliveryAndDedupTests(ReminderTestCase):
    def test_send_creates_reminder_and_event(self) -> None:
        today = date.today()
        if today.day < 5:
            self.skipTest("monthly start day gate not reached today")
        findings = reminders.scan_findings(self.connection)
        self.assertTrue(findings)
        key = findings[0].dedupe_key
        reminder_id = reminders.deliver_finding(self.connection, key, actor=self.admin)
        self.assertIsNotNone(reminder_id)
        row = self.connection.execute(
            "SELECT send_count, status FROM reminders WHERE id = ?", (reminder_id,)
        ).fetchone()
        self.assertEqual(1, row["send_count"])
        self.assertEqual("open", row["status"])
        events = self.connection.execute(
            "SELECT event_type FROM reminder_events WHERE reminder_id = ?", (reminder_id,)
        ).fetchall()
        self.assertEqual(["sent"], [item["event_type"] for item in events])

    def test_repeat_send_increments_count_without_new_row(self) -> None:
        today = date.today()
        if today.day < 5:
            self.skipTest("monthly start day gate not reached today")
        findings = reminders.scan_findings(self.connection)
        key = findings[0].dedupe_key
        first = reminders.deliver_finding(self.connection, key, actor=self.admin)
        second = reminders.deliver_finding(self.connection, key, actor=self.admin)
        self.assertEqual(first, second)
        row = self.connection.execute(
            "SELECT send_count FROM reminders WHERE dedupe_key = ?", (key,)
        ).fetchone()
        self.assertEqual(2, row["send_count"])
        count = self.connection.execute("SELECT COUNT(*) FROM reminders WHERE dedupe_key = ?", (key,)).fetchone()[0]
        self.assertEqual(1, count)
        events = self.connection.execute(
            "SELECT COUNT(*) FROM reminder_events WHERE reminder_id = ?", (first,)
        ).fetchone()[0]
        self.assertEqual(2, events)

    def test_manual_reminder_each_send_is_separate(self) -> None:
        first = reminders.send_manual(
            self.connection,
            company_id=self.company_a,
            display_type="其他",
            content="请尽快处理",
            deadline="2026-09-01",
            actor=self.admin,
        )
        second = reminders.send_manual(
            self.connection,
            company_id=self.company_a,
            display_type="其他",
            content="再次提醒",
            deadline=None,
            actor=self.admin,
        )
        self.assertNotEqual(first, second)


class IsolationTests(ReminderTestCase):
    def setUp(self) -> None:
        super().setUp()
        reminders.send_manual(
            self.connection,
            company_id=self.company_a,
            display_type="流水未提交",
            content="甲公司专属",
            deadline=None,
            actor=self.admin,
        )
        reminders.send_manual(
            self.connection,
            company_id=self.company_b,
            display_type="流水未提交",
            content="乙公司专属",
            deadline=None,
            actor=self.admin,
        )

    def test_company_sees_only_own_reminders(self) -> None:
        items_a = reminders.list_company_reminders(self.connection, self.company_a)
        items_b = reminders.list_company_reminders(self.connection, self.company_b)
        self.assertEqual(1, len(items_a))
        self.assertEqual(1, len(items_b))
        self.assertIn("甲公司", items_a[0]["title"])
        self.assertIn("乙公司", items_b[0]["title"])

    def test_company_cannot_update_other_company_reminder(self) -> None:
        other_id = reminders.list_company_reminders(self.connection, self.company_b)[0]["id"]
        ok = reminders.update_reminder_status(
            self.connection, other_id, "acknowledged", company_id=self.company_a, actor=self.actor_a
        )
        self.assertFalse(ok)


class AuditTrailTests(ReminderTestCase):
    def test_scan_writes_audit_log(self) -> None:
        reminders.run_scan(self.connection, actor=self.admin, ip="127.0.0.1")
        row = self.connection.execute(
            "SELECT action, detail FROM audit_log WHERE action = 'reminder_scan'"
        ).fetchone()
        self.assertIsNotNone(row)
        detail = json.loads(row["detail"])
        self.assertIn("total", detail)

    def test_events_are_append_only(self) -> None:
        reminder_id = reminders.send_manual(
            self.connection,
            company_id=self.company_a,
            display_type="测试",
            content="内容",
            deadline=None,
            actor=self.admin,
        )
        with self.assertRaises(Exception):
            with self.connection:
                self.connection.execute(
                    "UPDATE reminder_events SET detail = 'tampered' WHERE reminder_id = ?",
                    (reminder_id,),
                )
        with self.assertRaises(Exception):
            with self.connection:
                self.connection.execute(
                    "DELETE FROM reminder_events WHERE reminder_id = ?", (reminder_id,)
                )

    def test_reminders_cannot_be_deleted(self) -> None:
        reminder_id = reminders.send_manual(
            self.connection,
            company_id=self.company_a,
            display_type="测试",
            content="内容",
            deadline=None,
            actor=self.admin,
        )
        with self.assertRaises(Exception):
            with self.connection:
                self.connection.execute("DELETE FROM reminders WHERE id = ?", (reminder_id,))


class StatusFlowTests(ReminderTestCase):
    def test_acknowledge_and_resolve(self) -> None:
        reminder_id = reminders.send_manual(
            self.connection,
            company_id=self.company_a,
            display_type="待确认",
            content="请处理",
            deadline=None,
            actor=self.admin,
        )
        ok = reminders.update_reminder_status(
            self.connection, reminder_id, "acknowledged", company_id=self.company_a, actor=self.actor_a
        )
        self.assertTrue(ok)
        row = self.connection.execute(
            "SELECT status FROM reminders WHERE id = ?", (reminder_id,)
        ).fetchone()
        self.assertEqual("acknowledged", row["status"])
        ok = reminders.update_reminder_status(
            self.connection, reminder_id, "resolved", company_id=self.company_a, actor=self.actor_a
        )
        self.assertTrue(ok)
        events = self.connection.execute(
            "SELECT event_type FROM reminder_events WHERE reminder_id = ? ORDER BY id",
            (reminder_id,),
        ).fetchall()
        self.assertEqual(
            ["sent", "acknowledged", "resolved"],
            [item["event_type"] for item in events],
        )


class SettingsTests(ReminderTestCase):
    def test_default_settings_and_update(self) -> None:
        settings = reminders.get_settings(self.connection)
        self.assertEqual("5", settings["monthly_start_day"])
        self.assertEqual("5", settings["gap_days"])
        updated = reminders.update_settings(
            self.connection, {"gap_days": "7", "monthly_start_day": "6"}
        )
        self.assertEqual("7", updated["gap_days"])
        self.assertEqual("6", updated["monthly_start_day"])

    def test_unknown_setting_rejected(self) -> None:
        with self.assertRaises(ValueError):
            reminders.update_settings(self.connection, {"unknown_key": "1"})


class MigrationTests(unittest.TestCase):
    def test_v6_migration_applies_and_rolls_back(self) -> None:
        connection = connect(":memory:")
        self.addCleanup(connection.close)
        migrate(connection)
        versions = connection.execute(
            "SELECT version FROM schema_migrations ORDER BY version"
        ).fetchall()
        self.assertEqual(6, versions[-1]["version"])
        connection.execute("DELETE FROM schema_migrations WHERE version = 6")
        connection.executescript(
            """
            DROP TRIGGER IF EXISTS reminders_no_delete;
            DROP TRIGGER IF EXISTS reminder_events_no_delete;
            DROP TRIGGER IF EXISTS reminder_events_no_update;
            DROP TABLE IF EXISTS reminder_events;
            DROP TABLE IF EXISTS reminders;
            DROP TABLE IF EXISTS reminder_settings;
            """
        )
        row = connection.execute(
            "SELECT name FROM sqlite_master WHERE name = 'reminders'"
        ).fetchone()
        self.assertIsNone(row)
