# edited by glg
import sqlite3
import tempfile
import unittest
from datetime import datetime, timedelta
from pathlib import Path

import pypos.modules.penjualan.models.pembatalan_transaksi_model as pembatalan_model_mod
import pypos.modules.penjualan.models.settlement_model as settlement_model_mod
from pypos.modules.penjualan.controllers.pembatalan_transaksi_controller import (
    PembatalanTransaksiController,
)
from pypos.modules.penjualan.errors import PembatalanProcessError
from pypos.modules.penjualan.models.pembatalan_transaksi_model import (
    PembatalanTransaksiModel,
)


def _seed_cancel_schema(db_path: str):
    conn = sqlite3.connect(db_path)
    cur = conn.cursor()
    cur.execute(
        """
        CREATE TABLE IF NOT EXISTS transaksi (
            id INTEGER PRIMARY KEY,
            nomer TEXT,
            dtime TEXT,
            customers_nama TEXT,
            oleh_nama TEXT,
            transaksi_nilai REAL,
            settlement_id INTEGER DEFAULT 1,
            jenis_label TEXT DEFAULT 'invoice',
            trash INTEGER DEFAULT 0,
            cancel_dtime TEXT
        )
        """
    )
    cur.execute(
        """
        CREATE TABLE IF NOT EXISTS transaksi_data (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            transaksi_id TEXT,
            produk_id TEXT,
            produk_ord_jml INTEGER DEFAULT 0,
            trash INTEGER DEFAULT 0
        )
        """
    )
    cur.execute(
        """
        CREATE TABLE IF NOT EXISTS return_transaksi_penjualan (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            transaksi_id TEXT NOT NULL,
            refund_method TEXT NULL
        )
        """
    )
    cur.execute(
        """
        CREATE TABLE IF NOT EXISTS detail_return_transaksi_penjualan (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            return_id INTEGER NOT NULL,
            produk_id TEXT NOT NULL,
            jumlah INTEGER NOT NULL DEFAULT 0,
            harga REAL NOT NULL DEFAULT 0
        )
        """
    )
    cur.execute(
        """
        CREATE TABLE IF NOT EXISTS pembatalan_transaksi_history (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            transaksi_id TEXT,
            nomer TEXT,
            transaksi_dtime TEXT,
            customers_nama TEXT,
            kasir_nama TEXT,
            transaksi_nilai REAL,
            admin_verifikasi TEXT,
            dibatalkan_oleh_id TEXT,
            dibatalkan_oleh_nama TEXT,
            cancel_dtime TEXT
        )
        """
    )
    cur.execute(
        """
        CREATE TABLE IF NOT EXISTS settlement_history (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            tanggal TEXT,
            admin TEXT,
            kasir TEXT,
            total_harus REAL DEFAULT 0,
            total_disetor REAL DEFAULT 0,
            total_non_tunai REAL DEFAULT 0,
            total_refund_cash REAL DEFAULT 0,
            selisih REAL DEFAULT 0,
            status TEXT,
            data_transaksi_id TEXT
        )
        """
    )
    conn.commit()
    conn.close()


def _insert_transaksi(
    db_path: str,
    *,
    trx_id: int,
    nomer: str,
    dtime: str,
    settlement_id: int = 1,
    jenis_label: str = "invoice",
    trash: int = 0,
):
    conn = sqlite3.connect(db_path)
    cur = conn.cursor()
    cur.execute(
        """
        INSERT INTO transaksi
        (id, nomer, dtime, customers_nama, oleh_nama, transaksi_nilai, settlement_id, jenis_label, trash)
        VALUES (?, ?, ?, 'Cust Test', 'Kasir Test', 100000, ?, ?, ?)
        """,
        (trx_id, nomer, dtime, settlement_id, jenis_label, trash),
    )
    cur.execute(
        """
        INSERT INTO transaksi_data (transaksi_id, produk_id, produk_ord_jml, trash)
        VALUES (?, 'P-1', 2, 0)
        """,
        (str(trx_id),),
    )
    cur.execute(
        """
        INSERT INTO transaksi_data (transaksi_id, produk_id, produk_ord_jml, trash)
        VALUES (?, 'P-2', 1, 0)
        """,
        (str(trx_id),),
    )
    conn.commit()
    conn.close()


class PembatalanTransaksiControllerTests(unittest.TestCase):
    def setUp(self):
        self._tmpdir = tempfile.TemporaryDirectory()
        self.db_path = str(Path(self._tmpdir.name) / "cancel_guard.db")
        _seed_cancel_schema(self.db_path)

        self._orig_migrator = pembatalan_model_mod.run_schema_migrations_once
        self._orig_settlement_migrator = settlement_model_mod.run_schema_migrations_once
        pembatalan_model_mod.run_schema_migrations_once = lambda *_args, **_kwargs: None
        settlement_model_mod.run_schema_migrations_once = lambda *_args, **_kwargs: None
        try:
            self.model = PembatalanTransaksiModel(db_path=self.db_path)
            self.settlement_model = settlement_model_mod.SettlementModel(db_path=self.db_path)
        finally:
            pembatalan_model_mod.run_schema_migrations_once = self._orig_migrator

        self.controller = PembatalanTransaksiController(
            model=self.model,
            settlement_model=self.settlement_model,
            user_info={"id": 77, "nama": "Kasir QA"},
        )
        self.controller.penjualan_config_service.get_pembatalan_allowed_days = (
            lambda default=0: 0
        )
        self.controller.penjualan_config_service.get_pembatalan_search_limit = (
            lambda default=200: 200
        )

    def tearDown(self):
        settlement_model_mod.run_schema_migrations_once = self._orig_settlement_migrator
        self._tmpdir.cleanup()

    def _today(self) -> str:
        return datetime.now().strftime("%Y-%m-%d")

    def test_search_transaksi_hanya_muncul_yang_belum_settle(self):
        dtime = f"{self._today()} 10:00:00"
        _insert_transaksi(
            self.db_path,
            trx_id=1,
            nomer="INV-OPEN",
            dtime=dtime,
            settlement_id=1,
        )
        _insert_transaksi(
            self.db_path,
            trx_id=2,
            nomer="INV-SETTLED",
            dtime=dtime,
            settlement_id=0,
        )

        rows = self.model.search_transaksi("", self._today(), self._today(), limit=20)
        nomor = {str(row["nomer"]) for row in rows}

        self.assertIn("INV-OPEN", nomor)
        self.assertNotIn("INV-SETTLED", nomor)

    def test_delete_transaksi_menolak_jika_sudah_settle(self):
        dtime = f"{self._today()} 10:10:00"
        _insert_transaksi(
            self.db_path,
            trx_id=10,
            nomer="INV-S-10",
            dtime=dtime,
            settlement_id=0,
        )

        with self.assertRaisesRegex(
            PembatalanProcessError, "CANCEL_SETTLEMENT_LOCKED"
        ):
            self.model.delete_transaksi("10", admin_name="admin-settle")

    def test_delete_transaksi_sukses_menandai_trash_dan_history(self):
        dtime = f"{self._today()} 11:00:00"
        _insert_transaksi(
            self.db_path,
            trx_id=20,
            nomer="INV-20",
            dtime=dtime,
            settlement_id=1,
        )

        self.assertTrue(
            self.model.delete_transaksi(
                "20",
                admin_name="admin-ok",
                dibatalkan_oleh_id=77,
                dibatalkan_oleh_nama="Kasir QA",
            )
        )

        conn = sqlite3.connect(self.db_path)
        conn.row_factory = sqlite3.Row
        cur = conn.cursor()
        cur.execute("SELECT trash, cancel_dtime FROM transaksi WHERE id = 20")
        trx = cur.fetchone()
        cur.execute(
            "SELECT COUNT(1) AS c FROM transaksi_data WHERE transaksi_id = '20' AND COALESCE(trash, 0) = 1"
        )
        detail_count = int(cur.fetchone()["c"] or 0)
        cur.execute(
            """
            SELECT admin_verifikasi, dibatalkan_oleh_id, dibatalkan_oleh_nama
            FROM pembatalan_transaksi_history
            WHERE transaksi_id = '20'
            ORDER BY id DESC
            LIMIT 1
            """
        )
        hist = cur.fetchone()
        conn.close()

        self.assertEqual(int(trx["trash"]), 1)
        self.assertTrue(str(trx["cancel_dtime"] or "").strip())
        self.assertEqual(detail_count, 2)
        self.assertEqual(str(hist["admin_verifikasi"]), "admin-ok")
        self.assertEqual(str(hist["dibatalkan_oleh_id"]), "77")
        self.assertEqual(str(hist["dibatalkan_oleh_nama"]), "Kasir QA")

    def test_delete_transaksi_kedua_kali_ditolak_idempotent(self):
        dtime = f"{self._today()} 11:15:00"
        _insert_transaksi(
            self.db_path,
            trx_id=21,
            nomer="INV-21",
            dtime=dtime,
            settlement_id=1,
        )
        self.model.delete_transaksi("21", admin_name="admin-1")

        with self.assertRaisesRegex(PembatalanProcessError, "CANCEL_ALREADY_DONE"):
            self.model.delete_transaksi("21", admin_name="admin-2")

    def test_delete_transaksi_menolak_non_invoice(self):
        dtime = f"{self._today()} 11:30:00"
        _insert_transaksi(
            self.db_path,
            trx_id=30,
            nomer="RET-30",
            dtime=dtime,
            settlement_id=1,
            jenis_label="retur",
        )
        with self.assertRaisesRegex(
            PembatalanProcessError, "CANCEL_INVALID_JENIS_LABEL"
        ):
            self.model.delete_transaksi("30", admin_name="admin-retur")

    def test_controller_menolak_human_error_id_tidak_valid(self):
        invalid_ids = ["", "   ", None, "abc", "0", "-1"]
        for invalid_id in invalid_ids:
            ok, msg = self.controller.batalkan_transaksi(invalid_id, admin_name="admin")
            self.assertFalse(ok)
            self.assertEqual(msg, "ID transaksi tidak valid.")

    def test_controller_menolak_jika_sudah_settle(self):
        dtime = f"{self._today()} 12:00:00"
        _insert_transaksi(
            self.db_path,
            trx_id=40,
            nomer="INV-40",
            dtime=dtime,
            settlement_id=0,
        )
        ok, msg = self.controller.batalkan_transaksi("40", admin_name="admin-settle")
        self.assertFalse(ok)
        self.assertEqual(msg, "Transaksi sudah disettle dan tidak dapat dibatalkan.")

    def test_controller_menolak_jika_diluar_periode_batal(self):
        old_day = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
        dtime = f"{old_day} 08:15:00"
        _insert_transaksi(
            self.db_path,
            trx_id=50,
            nomer="INV-OLD",
            dtime=dtime,
            settlement_id=1,
        )
        ok, msg = self.controller.batalkan_transaksi("50", admin_name="admin-old")
        self.assertFalse(ok)
        self.assertEqual(msg, "Transaksi di luar periode pembatalan yang diizinkan.")

    def test_controller_can_batalkan_true_hanya_untuk_invoice_hari_ini_belum_settle(self):
        dtime_today = f"{self._today()} 09:10:00"
        _insert_transaksi(
            self.db_path,
            trx_id=60,
            nomer="INV-60",
            dtime=dtime_today,
            settlement_id=1,
        )
        _insert_transaksi(
            self.db_path,
            trx_id=61,
            nomer="INV-61",
            dtime=dtime_today,
            settlement_id=0,
        )

        self.assertTrue(self.controller.can_batalkan("60"))
        self.assertFalse(self.controller.can_batalkan("61"))

    # edited by glg
    def test_controller_menolak_batal_jika_terkunci_di_history_settlement(self):
        dtime = f"{self._today()} 13:00:00"
        _insert_transaksi(
            self.db_path,
            trx_id=80,
            nomer="INV-HIST-80",
            dtime=dtime,
            settlement_id=1,
        )
        conn = sqlite3.connect(self.db_path)
        cur = conn.cursor()
        cur.execute(
            """
            INSERT INTO settlement_history (tanggal, admin, kasir, status, data_transaksi_id)
            VALUES (?, 'admin', 'kasir', 'Sesuai', ?)
            """,
            (dtime, "[80]"),
        )
        conn.commit()
        conn.close()

        ok, msg = self.controller.batalkan_transaksi("80", admin_name="admin-hist")
        self.assertFalse(ok)
        self.assertEqual(msg, "Transaksi sudah disettle dan tidak dapat dibatalkan.")

    # edited by glg
    def test_controller_menolak_batal_jika_terkunci_di_history_map(self):
        dtime = f"{self._today()} 13:10:00"
        _insert_transaksi(
            self.db_path,
            trx_id=83,
            nomer="INV-MAP-83",
            dtime=dtime,
            settlement_id=1,
        )
        conn = sqlite3.connect(self.db_path)
        cur = conn.cursor()
        cur.execute(
            """
            CREATE TABLE IF NOT EXISTS settlement_history_transaksi_map (
                settlement_history_id INTEGER NOT NULL,
                transaksi_id INTEGER NOT NULL,
                PRIMARY KEY (settlement_history_id, transaksi_id)
            )
            """
        )
        cur.execute(
            "INSERT INTO settlement_history_transaksi_map (settlement_history_id, transaksi_id) VALUES (1, 83)"
        )
        conn.commit()
        conn.close()

        ok, msg = self.controller.batalkan_transaksi("83", admin_name="admin-map")
        self.assertFalse(ok)
        self.assertEqual(msg, "Transaksi sudah disettle dan tidak dapat dibatalkan.")

    # edited by glg
    def test_load_transaksi_menyembunyikan_nota_yang_terkunci_history_settlement(self):
        dtime = f"{self._today()} 13:30:00"
        _insert_transaksi(
            self.db_path,
            trx_id=81,
            nomer="INV-HIST-81",
            dtime=dtime,
            settlement_id=1,
        )
        _insert_transaksi(
            self.db_path,
            trx_id=82,
            nomer="INV-OPEN-82",
            dtime=dtime,
            settlement_id=1,
        )
        conn = sqlite3.connect(self.db_path)
        cur = conn.cursor()
        cur.execute(
            """
            INSERT INTO settlement_history (tanggal, admin, kasir, status, data_transaksi_id)
            VALUES (?, 'admin', 'kasir', 'Sesuai', ?)
            """,
            (dtime, "[81]"),
        )
        conn.commit()
        conn.close()

        rows = self.controller.load_transaksi(keyword="")
        nomor = {str(row.get("nomer") or "") for row in rows}
        self.assertNotIn("INV-HIST-81", nomor)
        self.assertIn("INV-OPEN-82", nomor)


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