from __future__ import annotations

import io
import unittest

from openpyxl import Workbook

from arr_ingestion.contracts import ArtifactRef
from arr_web.booking_uploads import ProgramBookingSourceCoordinator
from arr_web.contracts import PortalError
from booking_ingestion.excel_postgres import BookingSourceSummary
from booking_ingestion.excel_review import (
    BookingDraftItem,
    BookingDraftPage,
    BookingDraftSummary,
)


DRAFT_ID = "bookingdraft-" + "a" * 32


def valid_workbook() -> bytes:
    workbook = Workbook()
    worksheet = workbook.active
    worksheet.title = "Booking"
    worksheet.append(["NO", "Tour Code", "Tour Days", "โรงแรม"])
    worksheet.append([1, "SYN-G1", "", "酒店 (【U-TWN8.5】 2,【U-เตียงเสริม13】 1)"])
    output = io.BytesIO()
    workbook.save(output)
    workbook.close()
    return output.getvalue()


def bilingual_workbook(*, exact_headers: bool = True) -> bytes:
    workbook = Workbook()
    worksheet = workbook.active
    worksheet.title = "QBD"
    worksheet.append(["QBD export"])
    worksheet.append([])
    tour_header = "Tour Code / 主团号" if exact_headers else "Tour Code / 主团号（备用）"
    worksheet.append(
        [
            "Month",
            "NO.",
            tour_header,
            "Tour Days",
            "Raw Hotel Detail / วางข้อมูลที่นี่ / 酒店明细",
        ]
    )
    worksheet.append(
        [
            "2026-08",
            1,
            "SYN-G1",
            "",
            "酒店 (【U-TWN8.5】 2,【U-เตียงเสริม13】 1)",
        ]
    )
    output = io.BytesIO()
    workbook.save(output)
    workbook.close()
    return output.getvalue()


def summary(disposition: str = "imported_and_activated") -> BookingSourceSummary:
    return BookingSourceSummary(
        source_batch_id=7,
        source_kind="booking_excel",
        filename="Booking 报表.xlsx",
        source_rows=1,
        worksheet_count=1,
        distinct_group_codes=1,
        room_quantity=3,
        activated_at=None,
        disposition=disposition,
    )


def draft_item(
    *,
    item_id: int = 1,
    room_type: str | None = "U-TWN",
    status: str = "confirmed",
) -> BookingDraftItem:
    return BookingDraftItem(
        item_id=item_id,
        draft_id=DRAFT_ID,
        worksheet="Booking",
        row_no=2,
        item_no=item_id,
        tour_code="SYN-G1",
        room_type_raw="U-TWN8.5" if item_id == 1 else "U-เตียงเสริม13",
        room_type_code=room_type,
        quantity=2 if item_id == 1 else 1,
        review_status=status,
        automatic=status == "confirmed",
        source_fragment="【U-TWN8.5】 2",
        updated_at=None,
    )


def draft_page() -> BookingDraftPage:
    return BookingDraftPage(
        summary=BookingDraftSummary(
            draft_id=DRAFT_ID,
            filename="Booking 报表.xlsx",
            status="reviewing",
            source_rows=1,
            worksheet_count=1,
            distinct_group_codes=1,
            extracted_items=2,
            confirmed_items=1,
            pending_items=1,
            deleted_items=0,
            confirmed_room_quantity=2,
            created_at=None,
            updated_at=None,
        ),
        items=(
            draft_item(item_id=1),
            draft_item(item_id=2, room_type=None, status="pending"),
        ),
        total=2,
        limit=50,
        offset=0,
    )


class FakeStored:
    def __init__(self, reference: ArtifactRef) -> None:
        self.reference = reference

    def to_artifact_ref(self) -> ArtifactRef:
        return self.reference


class FakeStore:
    def __init__(self) -> None:
        self.calls: list[dict[str, object]] = []

    def upload_committed(self, **values: object) -> FakeStored:
        self.calls.append(values)
        return FakeStored(
            ArtifactRef(
                role="booking_source",
                file_kind="booking_excel",
                object_key=(
                    f"arr/jobs/{values['job_id']}/attempts/0001/committed/"
                    "booking_source/booking-source.xlsx"
                ),
                original_filename=str(values["original_filename"]),
                sha256=str(values["expected_sha256"]),
                byte_size=int(values["expected_byte_size"]),
                mime_type=(
                    "application/vnd.openxmlformats-officedocument."
                    "spreadsheetml.sheet"
                ),
            )
        )


class FakeRepository:
    def __init__(self, existing: BookingSourceSummary | None = None) -> None:
        self.existing = existing
        self.page: BookingDraftPage | None = None
        self.created: list[tuple[str, ArtifactRef, object]] = []
        self.updated: list[tuple[str, int, str, int]] = []
        self.deleted: list[tuple[str, int]] = []
        self.deleted_batches: list[tuple[str, tuple[int, ...]]] = []
        self.discarded: list[str] = []
        self.activated: list[str] = []

    def current_source(self) -> BookingSourceSummary | None:
        return self.existing

    def current_draft(self, *, limit: int = 50, offset: int = 0) -> BookingDraftPage | None:
        _ = (limit, offset)
        return self.page

    def create_draft(
        self,
        draft_id: str,
        source: ArtifactRef,
        document: object,
        *,
        limit: int = 50,
    ) -> BookingDraftPage:
        _ = limit
        self.created.append((draft_id, source, document))
        return draft_page()

    def update_item(
        self,
        draft_id: str,
        item_id: int,
        room_type: str,
        quantity: int,
    ) -> BookingDraftItem:
        self.updated.append((draft_id, item_id, room_type, quantity))
        return draft_item(item_id=item_id, room_type=room_type)

    def delete_item(self, draft_id: str, item_id: int) -> None:
        self.deleted.append((draft_id, item_id))

    def delete_items(self, draft_id: str, item_ids: object) -> None:
        assert isinstance(item_ids, tuple)
        self.deleted_batches.append((draft_id, item_ids))

    def discard_draft(self, draft_id: str) -> None:
        self.discarded.append(draft_id)

    def activate_draft(self, draft_id: str) -> BookingSourceSummary:
        self.activated.append(draft_id)
        return summary()


class ProgramBookingSourceCoordinatorTests(unittest.TestCase):
    def test_valid_upload_is_stored_privately_then_becomes_review_draft(self) -> None:
        store = FakeStore()
        repository = FakeRepository()
        coordinator = ProgramBookingSourceCoordinator(store, repository)

        result = coordinator.submit("Booking 报表.xlsx", valid_workbook())

        self.assertEqual(result["summary"]["filename"], "Booking 报表.xlsx")
        self.assertEqual(result["summary"]["pending_items"], 1)
        self.assertEqual(len(store.calls), 1)
        self.assertEqual(store.calls[0]["role"], "booking_source")
        self.assertTrue(str(store.calls[0]["job_id"]).startswith("bookingdraft-"))
        self.assertEqual(len(repository.created), 1)
        self.assertEqual(repository.created[0][1].original_filename, "Booking 报表.xlsx")

    def test_bilingual_header_upload_is_stored_then_becomes_review_draft(self) -> None:
        store = FakeStore()
        repository = FakeRepository()
        coordinator = ProgramBookingSourceCoordinator(store, repository)

        result = coordinator.submit("QBD 八月.xlsx", bilingual_workbook())

        self.assertEqual(result["summary"]["filename"], "Booking 报表.xlsx")
        self.assertEqual(result["summary"]["pending_items"], 1)
        self.assertEqual(len(store.calls), 1)
        self.assertEqual(store.calls[0]["original_filename"], "QBD 八月.xlsx")
        self.assertEqual(len(repository.created), 1)

    def test_unrecognized_bilingual_header_never_reaches_storage_or_database(self) -> None:
        store = FakeStore()
        repository = FakeRepository()
        coordinator = ProgramBookingSourceCoordinator(store, repository)

        with self.assertRaises(PortalError) as raised:
            coordinator.submit("not.xlsx", bilingual_workbook(exact_headers=False))

        self.assertEqual(raised.exception.code, "BOOKING_EXCEL_HEADERS_MISSING")
        self.assertEqual(store.calls, [])
        self.assertEqual(repository.created, [])

    def test_reextracting_same_content_still_creates_a_fresh_draft(self) -> None:
        store = FakeStore()
        repository = FakeRepository(summary())
        coordinator = ProgramBookingSourceCoordinator(store, repository)

        coordinator.submit("again.xlsx", valid_workbook())
        coordinator.submit("again.xlsx", valid_workbook())

        self.assertEqual(len(store.calls), 2)
        self.assertEqual(len(repository.created), 2)
        self.assertNotEqual(repository.created[0][0], repository.created[1][0])

    def test_invalid_workbook_never_reaches_storage_or_database(self) -> None:
        store = FakeStore()
        repository = FakeRepository()
        coordinator = ProgramBookingSourceCoordinator(store, repository)

        with self.assertRaises(PortalError) as raised:
            coordinator.submit("not.xlsx", b"invalid")

        self.assertEqual(raised.exception.code, "BOOKING_EXCEL_INVALID")
        self.assertEqual(store.calls, [])
        self.assertEqual(repository.created, [])

    def test_review_actions_delegate_and_return_safe_results(self) -> None:
        repository = FakeRepository()
        repository.page = draft_page()
        coordinator = ProgramBookingSourceCoordinator(FakeStore(), repository)

        self.assertEqual(coordinator.draft()["summary"]["draft_id"], DRAFT_ID)
        updated = coordinator.update_item(DRAFT_ID, 2, "EXTRA BED", 1)
        self.assertEqual(updated["room_type"], "EXTRA BED")
        self.assertTrue(coordinator.delete_item(DRAFT_ID, 2)["deleted"])
        batch = coordinator.delete_items(DRAFT_ID, [3, 4])
        self.assertEqual(batch["deleted_count"], 2)
        self.assertEqual(batch["item_ids"], [3, 4])
        self.assertTrue(coordinator.discard(DRAFT_ID)["discarded"])
        self.assertEqual(coordinator.activate(DRAFT_ID)["source_batch_id"], 7)
        self.assertEqual(repository.updated, [(DRAFT_ID, 2, "EXTRA BED", 1)])
        self.assertEqual(
            repository.deleted_batches,
            [(DRAFT_ID, (2,)), (DRAFT_ID, (3, 4))],
        )
        self.assertEqual(repository.discarded, [DRAFT_ID])
        self.assertEqual(repository.activated, [DRAFT_ID])

    def test_current_hides_historical_internal_filename(self) -> None:
        legacy = BookingSourceSummary(
            source_batch_id=1,
            source_kind="expected_fixture",
            filename="internal-fixture.md",
            source_rows=867,
            worksheet_count=6,
            distinct_group_codes=348,
            room_quantity=867,
            activated_at=None,
        )
        coordinator = ProgramBookingSourceCoordinator(FakeStore(), FakeRepository(legacy))

        result = coordinator.current()

        self.assertIsNotNone(result)
        self.assertEqual(result["source_type"], "historical")
        self.assertIsNone(result["filename"])

    def test_bulk_delete_rejects_duplicate_items_before_repository_call(self) -> None:
        repository = FakeRepository()
        coordinator = ProgramBookingSourceCoordinator(FakeStore(), repository)

        with self.assertRaises(PortalError) as raised:
            coordinator.delete_items(DRAFT_ID, [2, 2])

        self.assertEqual(raised.exception.code, "BOOKING_EXCEL_REVIEW_ITEMS_INVALID")
        self.assertEqual(repository.deleted_batches, [])


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