from __future__ import annotations

import unittest
from datetime import date
from decimal import Decimal
from typing import Dict, List, Optional

from company_reports.contracts import (
    BatchSnapshot,
    BookingRoomItem,
    DailyVersionPin,
    ErrorCode,
    FinanceFact,
    WarningCode,
)
from company_reports.core import (
    build_company_report,
    company_for_fact,
    normalize_group_code,
)


def finance_fact(
    record_id: int,
    *,
    daily_version_id: int = 1000,
    group: str = "SYN-GROUP-A",
    arrival: date = date(2026, 7, 8),
    departure: date = date(2026, 7, 10),
    nights: int = 2,
    room_label: str = "SYN-RM2",
    total_price: Decimal = Decimal("900"),
    block_code: str = "SYN-BLOCK-A",
    company_key: str = "QBD",
    channel_key: str = "QBD",
) -> FinanceFact:
    return FinanceFact(
        daily_record_id=record_id,
        daily_version_id=daily_version_id,
        business_date=date(2026, 7, 9),
        channel_key=channel_key,
        company_key=company_key,
        company_name="SYN-COMPANY",
        block_code=block_code,
        group_code_key=group,
        res_comment=group,
        room_category_label=room_label,
        arrival=arrival,
        departure=departure,
        nights=nights,
        total_price=total_price,
        booking_source_match_status="matched",
    )


def room_item(
    item_no: int,
    *,
    group: str = "SYN-GROUP-A",
    arrival: date = date(2026, 7, 8),
    departure: date = date(2026, 7, 10),
    nights: int = 2,
    room_type: str = "SYN-SUP-TWN",
    quantity: int = 1,
    parse_version_id: int = 700,
    segment_no: int = 1,
) -> BookingRoomItem:
    return BookingRoomItem(
        group_code_key=group,
        parse_version_id=parse_version_id,
        segment_no=segment_no,
        arrival=arrival,
        departure=departure,
        nights=nights,
        item_no=item_no,
        room_type_raw=room_type,
        quantity=quantity,
    )


def snapshot(
    facts: List[FinanceFact],
    items: List[BookingRoomItem],
    parse_versions: Optional[Dict[str, int]] = None,
) -> BatchSnapshot:
    pins_by_date = {fact.business_date: fact.daily_version_id for fact in facts}
    pins = tuple(
        DailyVersionPin(business_date, version_id)
        for business_date, version_id in sorted(pins_by_date.items())
    )
    return BatchSnapshot(
        facts=tuple(facts),
        room_items=tuple(items),
        daily_versions=pins,
        group_parse_versions=parse_versions
        if parse_versions is not None
        else {"SYN-GROUP-A": 700},
    )


class CompanyReportCoreTests(unittest.TestCase):
    def test_group_code_normalization_and_company_mapping_precedence(self):
        self.assertEqual(normalize_group_code("  syn－group-a  "), "SYN-GROUP-A")

        cases = (
            ("LIAN TAI", "OTHER", "LianTai"),
            ("QBD", "OTHER", "QBD"),
            ("UNMAPPED", "DY-AI-Easy-KB", "DY-AI-Easy-KB"),
            ("FENGRUN", "OTHER", "FengRun"),
            ("HANA TOUR", "OTHER", "HanaTour"),
        )
        for company_key, channel_key, expected in cases:
            with self.subTest(expected=expected):
                self.assertEqual(
                    company_for_fact(
                        finance_fact(
                            1,
                            company_key=company_key,
                            channel_key=channel_key,
                        )
                    ),
                    expected,
                )

    def test_same_stay_is_aggregated_with_static_prices_and_room_quantities(self):
        facts = [
            finance_fact(1),
            finance_fact(2, block_code="SYN-BLOCK-B"),
            finance_fact(
                3,
                room_label="SYN-RM3",
                total_price=Decimal("3400"),
                block_code="SYN-BLOCK-A",
            ),
        ]
        items = [
            room_item(1, quantity=1),
            room_item(2, quantity=1),
            room_item(3, room_type="SYN-SUP-TRP", quantity=1),
        ]

        report = build_company_report(
            "QBD", 2026, 7, date(2026, 7, 10), snapshot(facts, items)
        )

        self.assertTrue(report.valid)
        self.assertEqual(report.row_count, 1)
        row = report.periods[0].rows[0]
        self.assertEqual(row.block_code, "SYN-BLOCK-A；SYN-BLOCK-B")
        self.assertEqual(row.booking_room, "【SYN-SUP-TWN】2；【SYN-SUP-TRP】1")
        self.assertEqual(
            row.total_booking_price,
            "【SYN-RM2】900×2 + 【SYN-RM3】3,400×1 = 5,200",
        )
        self.assertFalse(row.duplicate_group)
        self.assertFalse(row.multi_price_review)
        self.assertFalse(report.periods[1].active)
        self.assertEqual(report.periods[1].rows, ())

    def test_same_group_with_different_stays_is_split_and_highlighted(self):
        second_arrival = date(2026, 7, 17)
        second_departure = date(2026, 7, 21)
        facts = [
            finance_fact(1),
            finance_fact(
                2,
                arrival=second_arrival,
                departure=second_departure,
                nights=4,
                total_price=Decimal("1800"),
            ),
        ]
        items = [
            room_item(1),
            room_item(
                2,
                arrival=second_arrival,
                departure=second_departure,
                nights=4,
                quantity=2,
                segment_no=2,
            ),
        ]

        report = build_company_report(
            "QBD", 2026, 7, date(2026, 7, 31), snapshot(facts, items)
        )

        self.assertTrue(report.valid)
        self.assertEqual([len(period.rows) for period in report.periods], [1, 0, 1])
        all_rows = [row for period in report.periods for row in period.rows]
        self.assertTrue(all(row.duplicate_group for row in all_rows))
        self.assertEqual(
            all_rows[1].total_booking_price,
            "【SYN-RM2】1,800×1 = 1,800",
        )

    def test_same_room_type_with_multiple_static_prices_warns_but_succeeds(self):
        facts = [
            finance_fact(1, total_price=Decimal("900")),
            finance_fact(2, total_price=Decimal("1800")),
        ]
        report = build_company_report(
            "QBD", 2026, 7, date(2026, 7, 10), snapshot(facts, [room_item(1)])
        )

        self.assertTrue(report.valid)
        self.assertEqual(
            [warning.code for warning in report.warnings],
            [WarningCode.MULTI_PRICE_REVIEW],
        )
        row = report.periods[0].rows[0]
        self.assertTrue(row.multi_price_review)
        self.assertEqual(
            row.total_booking_price,
            "【SYN-RM2】900×1 + 【SYN-RM2】1,800×1 = 2,700",
        )

    def test_missing_group_code_keeps_each_fact_with_blank_booking_room(self):
        facts = [
            finance_fact(
                2,
                group="",
                block_code="SYN-BLOCK-B",
                total_price=Decimal("1800"),
            ),
            finance_fact(
                1,
                group="",
                block_code="SYN-BLOCK-A",
                total_price=Decimal("900"),
            ),
        ]

        report = build_company_report(
            "QBD", 2026, 7, date(2026, 7, 10), snapshot(facts, [], {})
        )

        self.assertTrue(report.valid)
        self.assertEqual(report.errors, ())
        self.assertEqual(report.row_count, 2)
        rows = report.periods[0].rows
        self.assertEqual([row.record_ids for row in rows], [(1,), (2,)])
        self.assertTrue(all(row.booking_room == "" for row in rows))
        self.assertTrue(all(row.res_comment == "" for row in rows))
        self.assertTrue(all(not row.duplicate_group for row in rows))
        self.assertEqual(
            [row.total_booking_price for row in rows],
            ["【SYN-RM2】900×1 = 900", "【SYN-RM2】1,800×1 = 1,800"],
        )

    def test_unmatched_group_code_keeps_segment_with_blank_booking_room(self):
        facts = [
            finance_fact(1, block_code="SYN-BLOCK-A", total_price=Decimal("900")),
            finance_fact(2, block_code="SYN-BLOCK-B", total_price=Decimal("1800")),
        ]
        cases = (
            ({}, {}),
            ({"SYN-GROUP-A": 700}, {"SYN-GROUP-A": 700}),
        )

        for parse_versions, expected_booking_versions in cases:
            with self.subTest(parse_versions=parse_versions):
                report = build_company_report(
                    "QBD",
                    2026,
                    7,
                    date(2026, 7, 10),
                    snapshot(facts, [], parse_versions),
                )

                self.assertTrue(report.valid)
                self.assertEqual(report.errors, ())
                self.assertEqual(report.row_count, 1)
                row = report.periods[0].rows[0]
                self.assertEqual(row.res_comment, "SYN-GROUP-A")
                self.assertEqual(row.booking_room, "")
                self.assertEqual(row.block_code, "SYN-BLOCK-A；SYN-BLOCK-B")
                self.assertEqual(
                    row.total_booking_price,
                    "【SYN-RM2】900×1 + 【SYN-RM2】1,800×1 = 2,700",
                )
                self.assertTrue(row.multi_price_review)
                self.assertEqual(
                    [warning.code for warning in report.warnings],
                    [WarningCode.MULTI_PRICE_REVIEW],
                )
                self.assertEqual(report.booking_versions, expected_booking_versions)

    def test_invalid_source_facts_block_the_company_report(self):
        cases = (
            (
                finance_fact(1, nights=3),
                [room_item(1)],
                {"SYN-GROUP-A": 700},
                ErrorCode.NIGHTS_CONFLICT,
            ),
            (
                finance_fact(1, total_price=Decimal("-1")),
                [room_item(1)],
                {"SYN-GROUP-A": 700},
                ErrorCode.TOTAL_PRICE_INVALID,
            ),
        )
        for fact, items, versions, expected in cases:
            with self.subTest(expected=expected):
                report = build_company_report(
                    "QBD",
                    2026,
                    7,
                    date(2026, 7, 10),
                    snapshot([fact], items, versions),
                )
                self.assertFalse(report.valid)
                self.assertIn(expected, {error.code for error in report.errors})
                with self.assertRaises(ValueError):
                    report.to_workbook_payload()

    def test_departure_controls_period_and_rows_are_deterministic(self):
        facts = [
            finance_fact(
                2,
                group="SYN-GROUP-B",
                arrival=date(2026, 7, 9),
                departure=date(2026, 7, 11),
            ),
            finance_fact(
                1,
                arrival=date(2026, 7, 6),
                departure=date(2026, 7, 10),
                nights=4,
            ),
        ]
        items = [
            room_item(
                1,
                arrival=date(2026, 7, 6),
                departure=date(2026, 7, 10),
                nights=4,
            ),
            room_item(
                2,
                group="SYN-GROUP-B",
                arrival=date(2026, 7, 9),
                departure=date(2026, 7, 11),
            ),
        ]
        report = build_company_report(
            "QBD",
            2026,
            7,
            date(2026, 7, 20),
            snapshot(
                facts,
                items,
                {"SYN-GROUP-A": 700, "SYN-GROUP-B": 700},
            ),
        )

        self.assertTrue(report.valid)
        self.assertEqual([len(period.rows) for period in report.periods], [1, 1, 0])
        self.assertEqual(report.periods[0].rows[0].departure, date(2026, 7, 10))
        self.assertEqual(report.periods[1].rows[0].departure, date(2026, 7, 11))

    def test_empty_company_still_has_three_header_only_periods(self):
        report = build_company_report(
            "HanaTour",
            2026,
            2,
            date(2026, 2, 28),
            snapshot([], [], {}),
        )

        self.assertTrue(report.valid)
        self.assertEqual(report.filename, "HanaTour-February-2026.xlsx")
        self.assertEqual(
            [period.key for period in report.periods],
            ["01-10", "11-20", "21-28"],
        )
        self.assertEqual(report.row_count, 0)

    def test_month_names_are_fixed_english_contract_values(self):
        report = build_company_report(
            "FengRun",
            2026,
            9,
            date(2026, 9, 10),
            snapshot([], [], {}),
        )
        self.assertEqual(report.filename, "FengRun-September-2026.xlsx")
        self.assertEqual(report.periods[0].sheet_name, "FengRun 01-10 Sep 2026")


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