# edited by glg
import sqlite3
import tempfile
import unittest
from pathlib import Path

import pytest

from pypos.modules.penjualan.services.free_produk_sync_outbox_service import (
    FreeProdukSyncOutboxService,
)

pytestmark = [pytest.mark.unit, pytest.mark.critical_flow, pytest.mark.retry]


class FreeProdukSyncOutboxServiceTests(unittest.TestCase):
    def test_enqueue_claim_and_mark_sent(self):
        with tempfile.TemporaryDirectory() as td:
            db_path = str(Path(td) / "outbox_fp.db")
            service = FreeProdukSyncOutboxService(db_path=db_path)

            queued = service.enqueue_payload(
                [{"transaksi_id": 1, "produk_id": 10, "free_produk_id": 11, "free_qty": 1}],
                reason="unit_test",
            )
            self.assertTrue(bool(queued.get("queued")))
            self.assertGreater(int(queued.get("id") or 0), 0)
            self.assertEqual(service.count_pending(), 1)

            due = service.claim_due_payloads(max_items=3)
            self.assertEqual(len(due), 1)
            self.assertEqual(int(due[0]["id"]), int(queued["id"]))
            self.assertEqual(int(due[0]["attempt_count"]), 0)

            service.mark_sent(int(queued["id"]))
            self.assertEqual(service.count_pending(), 0)

    def test_mark_retry_updates_attempt_and_status(self):
        with tempfile.TemporaryDirectory() as td:
            db_path = str(Path(td) / "outbox_fp_retry.db")
            service = FreeProdukSyncOutboxService(db_path=db_path)

            queued = service.enqueue_payload(
                [{"transaksi_id": 2, "produk_id": 20, "free_produk_id": 21, "free_qty": 2}],
                reason="unit_test_retry",
            )
            outbox_id = int(queued.get("id") or 0)
            self.assertGreater(outbox_id, 0)

            service.mark_retry(
                outbox_id=outbox_id,
                attempt_count=2,
                error_code="HTTP_500",
                error_message="server_error",
            )

            conn = sqlite3.connect(db_path)
            conn.row_factory = sqlite3.Row
            cur = conn.cursor()
            cur.execute(
                """
                SELECT status, attempt_count, last_error_code, last_error_message
                FROM free_produk_sync_outbox
                WHERE id = ?
                """,
                (outbox_id,),
            )
            row = cur.fetchone()
            conn.close()

            self.assertIsNotNone(row)
            self.assertEqual(str(row["status"]), "RETRY")
            self.assertEqual(int(row["attempt_count"]), 2)
            self.assertEqual(str(row["last_error_code"]), "HTTP_500")
            self.assertEqual(str(row["last_error_message"]), "server_error")

    def test_claim_due_payloads_tidak_duplikasi_saat_inflight(self):
        with tempfile.TemporaryDirectory() as td:
            db_path = str(Path(td) / "outbox_fp_inflight.db")
            service = FreeProdukSyncOutboxService(db_path=db_path)

            queued = service.enqueue_payload(
                [{"transaksi_id": 3, "produk_id": 30, "free_produk_id": 31, "free_qty": 1}],
                reason="unit_test_inflight",
            )
            outbox_id = int(queued.get("id") or 0)
            self.assertGreater(outbox_id, 0)

            first_claim = service.claim_due_payloads(max_items=1)
            second_claim = service.claim_due_payloads(max_items=1)

            self.assertEqual(len(first_claim), 1)
            self.assertEqual(int(first_claim[0]["id"]), outbox_id)
            self.assertEqual(len(second_claim), 0)
            self.assertEqual(service.count_inflight(), 1)

    def test_claim_due_payloads_requeue_stale_inflight(self):
        with tempfile.TemporaryDirectory() as td:
            db_path = str(Path(td) / "outbox_fp_stale.db")
            service = FreeProdukSyncOutboxService(db_path=db_path)

            queued = service.enqueue_payload(
                [{"transaksi_id": 4, "produk_id": 40, "free_produk_id": 41, "free_qty": 2}],
                reason="unit_test_stale",
            )
            outbox_id = int(queued.get("id") or 0)
            self.assertGreater(outbox_id, 0)

            first_claim = service.claim_due_payloads(max_items=1)
            self.assertEqual(len(first_claim), 1)

            conn = sqlite3.connect(db_path)
            cur = conn.cursor()
            cur.execute(
                """
                UPDATE free_produk_sync_outbox
                SET status = 'INFLIGHT', lease_until = '2000-01-01 00:00:00'
                WHERE id = ?
                """,
                (outbox_id,),
            )
            conn.commit()
            conn.close()

            second_claim = service.claim_due_payloads(max_items=1)
            self.assertEqual(len(second_claim), 1)
            self.assertEqual(int(second_claim[0]["id"]), outbox_id)
            self.assertEqual(str(second_claim[0]["status"]), "INFLIGHT")

    @pytest.mark.chaos
    def test_recover_stale_inflight_leases_chaos_recover(self):
        with tempfile.TemporaryDirectory() as td:
            db_path = str(Path(td) / "outbox_fp_recover_stale.db")
            service = FreeProdukSyncOutboxService(db_path=db_path)

            queued = service.enqueue_payload(
                [{"transaksi_id": 44, "produk_id": 440, "free_produk_id": 441, "free_qty": 2}],
                reason="unit_test_recover_stale",
            )
            outbox_id = int(queued.get("id") or 0)
            self.assertGreater(outbox_id, 0)

            _ = service.claim_due_payloads(max_items=1)
            conn = sqlite3.connect(db_path)
            cur = conn.cursor()
            cur.execute(
                """
                UPDATE free_produk_sync_outbox
                SET status = 'INFLIGHT', lease_until = '2001-01-01 00:00:00'
                WHERE id = ?
                """,
                (outbox_id,),
            )
            conn.commit()
            conn.close()

            affected = service.recover_stale_inflight_leases()
            self.assertGreaterEqual(int(affected), 1)

            rows = service.claim_due_payloads(max_items=1)
            self.assertEqual(len(rows), 1)
            self.assertEqual(int(rows[0]["id"]), outbox_id)

    @pytest.mark.chaos
    def test_claim_due_payloads_chaos_invalid_json_tetap_aman(self):
        with tempfile.TemporaryDirectory() as td:
            db_path = str(Path(td) / "outbox_fp_invalid_json.db")
            service = FreeProdukSyncOutboxService(db_path=db_path)

            queued = service.enqueue_payload(
                [{"transaksi_id": 45, "produk_id": 450, "free_produk_id": 451, "free_qty": 1}],
                reason="unit_test_invalid_json",
            )
            outbox_id = int(queued.get("id") or 0)
            self.assertGreater(outbox_id, 0)

            conn = sqlite3.connect(db_path)
            cur = conn.cursor()
            cur.execute(
                """
                UPDATE free_produk_sync_outbox
                SET payload_json = ?, status = 'PENDING', available_at = '2000-01-01 00:00:00'
                WHERE id = ?
                """,
                ("{invalid_json", outbox_id),
            )
            conn.commit()
            conn.close()

            claimed = service.claim_due_payloads(max_items=1)
            self.assertEqual(len(claimed), 1)
            self.assertEqual(claimed[0]["payload"], [])

    def test_mark_retry_jadi_dead_saat_melebihi_batas_attempt(self):
        with tempfile.TemporaryDirectory() as td:
            db_path = str(Path(td) / "outbox_fp_dead.db")
            service = FreeProdukSyncOutboxService(db_path=db_path, max_attempts=2)

            queued = service.enqueue_payload(
                [{"transaksi_id": 5, "produk_id": 50, "free_produk_id": 51, "free_qty": 1}],
                reason="unit_test_dead",
            )
            outbox_id = int(queued.get("id") or 0)
            self.assertGreater(outbox_id, 0)

            service.mark_retry(
                outbox_id=outbox_id,
                attempt_count=2,
                error_code="HTTP_503",
                error_message="service_unavailable",
            )

            conn = sqlite3.connect(db_path)
            conn.row_factory = sqlite3.Row
            cur = conn.cursor()
            cur.execute(
                """
                SELECT status, attempt_count, last_error_code, last_error_message
                FROM free_produk_sync_outbox
                WHERE id = ?
                """,
                (outbox_id,),
            )
            row = cur.fetchone()
            conn.close()

            self.assertIsNotNone(row)
            self.assertEqual(str(row["status"]), "DEAD")
            self.assertEqual(int(row["attempt_count"]), 2)
            self.assertEqual(str(row["last_error_code"]), "HTTP_503")
            self.assertEqual(str(row["last_error_message"]), "service_unavailable")
            self.assertEqual(service.count_pending(), 0)
            self.assertEqual(service.count_dead(), 1)

    def test_enqueue_payload_dedup_saat_payload_aktif_sama(self):
        with tempfile.TemporaryDirectory() as td:
            db_path = str(Path(td) / "outbox_fp_dedup_active.db")
            service = FreeProdukSyncOutboxService(db_path=db_path)
            payload = [{"transaksi_id": 6, "produk_id": 60, "free_produk_id": 61, "free_qty": 1}]

            first = service.enqueue_payload(payload, reason="unit_test_dedup_1")
            second = service.enqueue_payload(payload, reason="unit_test_dedup_2")

            self.assertTrue(bool(first.get("queued")))
            self.assertFalse(bool(second.get("queued")))
            self.assertEqual(int(first.get("id") or 0), int(second.get("id") or 0))
            self.assertEqual(service.count_pending(), 1)

    def test_enqueue_payload_boleh_ulang_setelah_sent(self):
        with tempfile.TemporaryDirectory() as td:
            db_path = str(Path(td) / "outbox_fp_dedup_sent.db")
            service = FreeProdukSyncOutboxService(db_path=db_path)
            payload = [{"transaksi_id": 7, "produk_id": 70, "free_produk_id": 71, "free_qty": 1}]

            first = service.enqueue_payload(payload, reason="unit_test_sent_1")
            first_id = int(first.get("id") or 0)
            self.assertTrue(bool(first.get("queued")))
            self.assertGreater(first_id, 0)
            service.mark_sent(first_id)

            second = service.enqueue_payload(payload, reason="unit_test_sent_2")
            second_id = int(second.get("id") or 0)
            self.assertTrue(bool(second.get("queued")))
            self.assertGreater(second_id, 0)
            self.assertNotEqual(first_id, second_id)
            self.assertEqual(service.count_pending(), 1)

    def test_build_dead_letter_alert_memuat_ringkasan_error_utama(self):
        with tempfile.TemporaryDirectory() as td:
            db_path = str(Path(td) / "outbox_fp_alert.db")
            service = FreeProdukSyncOutboxService(db_path=db_path, max_attempts=1)
            queued = service.enqueue_payload(
                [{"transaksi_id": 8, "produk_id": 80, "free_produk_id": 81, "free_qty": 1}],
                reason="unit_test_alert",
            )
            outbox_id = int(queued.get("id") or 0)
            self.assertGreater(outbox_id, 0)

            service.mark_dead(
                outbox_id=outbox_id,
                attempt_count=3,
                error_code="HTTP_500",
                error_message="server_error",
            )

            alert_payload = service.build_dead_letter_alert(limit=3)
            self.assertEqual(int(alert_payload.get("dead_count") or 0), 1)
            self.assertTrue(bool(alert_payload.get("top_items")))
            self.assertIn("code=HTTP_500", str(alert_payload.get("top_items_text") or ""))

    def test_purge_terminal_hapus_hanya_status_terminal_yang_melewati_retensi(self):
        with tempfile.TemporaryDirectory() as td:
            db_path = str(Path(td) / "outbox_fp_retention.db")
            service = FreeProdukSyncOutboxService(db_path=db_path)

            sent = service.enqueue_payload([{"transaksi_id": 9}], reason="retention_sent")
            dead = service.enqueue_payload([{"transaksi_id": 10}], reason="retention_dead")
            pending = service.enqueue_payload([{"transaksi_id": 11}], reason="retention_pending")
            sent_id = int(sent.get("id") or 0)
            dead_id = int(dead.get("id") or 0)
            pending_id = int(pending.get("id") or 0)
            self.assertGreater(sent_id, 0)
            self.assertGreater(dead_id, 0)
            self.assertGreater(pending_id, 0)

            service.mark_sent(sent_id)
            service.mark_dead(dead_id, attempt_count=3, error_code="FAIL", error_message="fail")

            conn = sqlite3.connect(db_path)
            cur = conn.cursor()
            cur.execute(
                """
                UPDATE free_produk_sync_outbox
                SET created_at = '2000-01-01 00:00:00',
                    updated_at = '2000-01-01 00:00:00'
                WHERE id IN (?, ?)
                """,
                (sent_id, dead_id),
            )
            conn.commit()
            conn.close()

            summary = service.purge_terminal(retention_days=30, limit=100)
            self.assertEqual(int(summary.get("purged_sent") or 0), 1)
            self.assertEqual(int(summary.get("purged_dead") or 0), 1)
            self.assertEqual(service.count_pending(), 1)

            conn = sqlite3.connect(db_path)
            conn.row_factory = sqlite3.Row
            cur = conn.cursor()
            cur.execute(
                """
                SELECT id, status
                FROM free_produk_sync_outbox
                ORDER BY id ASC
                """
            )
            rows = cur.fetchall()
            conn.close()
            self.assertEqual(len(rows), 1)
            self.assertEqual(int(rows[0]["id"]), pending_id)
            self.assertEqual(str(rows[0]["status"]), "PENDING")


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