from __future__ import annotations

import unittest
from datetime import date
from decimal import Decimal

from monthly_reports.contracts import (
    KB_HEADER,
    ChannelObservation,
    DailyVersionPin,
    ErrorCode,
    MonthlyFact,
    MonthlyReportError,
    MonthlySnapshot,
    STANDARD_SHEETS,
)
from monthly_reports.core import build_monthly_report


def fact(
    record_id: int,
    *,
    business_date: date = date(2026, 7, 2),
    daily_version_id: int = 102,
    channel: str = "QBD",
    rooms: int = 1,
    nights: int = 2,
    real_price: Decimal = Decimal("450.00"),
) -> MonthlyFact:
    return MonthlyFact(
        daily_record_id=record_id,
        daily_version_id=daily_version_id,
        business_date=business_date,
        channel_key=channel,
        arrival=business_date,
        departure=date.fromordinal(business_date.toordinal() + nights),
        nights=nights,
        adults=2,
        children=0,
        block_code="SYN-BLOCK",
        no_of_rooms=rooms,
        company_name="SYN-COMPANY",
        confirmation_no=f"SYN-{record_id}",
        disp_room_no=f"SYN-RM-{record_id}",
        effective_rate_amount=real_price,
        full_name="SYN-GUEST",
        res_comment="SYN-COMMENT",
        trace_text="",
        products="",
        rate_code="SYN-RATE",
        room_category_label="SYN-TYPE",
        real_price=real_price,
        total_price=real_price * rooms * nights,
        kb_amount=Decimal(rooms * 100) if channel == "DY-AI-Easy-KB" else None,
    )


def snapshot(*facts: MonthlyFact, preferred: tuple[str, ...] = ()) -> MonthlySnapshot:
    pins = tuple(
        DailyVersionPin(day, version)
        for day, version in sorted(
            {item.business_date: item.daily_version_id for item in facts}.items()
        )
    )
    counts: dict[tuple[date, int, str], int] = {}
    for item in facts:
        key = (item.business_date, item.daily_version_id, item.channel_key)
        counts[key] = counts.get(key, 0) + 1
    observations = tuple(
        ChannelObservation(day, version, channel, None, count)
        for (day, version, channel), count in sorted(counts.items())
    )
    return MonthlySnapshot(tuple(facts), pins, observations, preferred)


class MonthlyReportsCoreTests(unittest.TestCase):
    def test_standard_sheets_are_retained_and_unknown_order_is_stable(self):
        report = build_monthly_report(
            2026,
            7,
            date(2026, 7, 2),
            snapshot(
                fact(2, channel="T- HANATOUR TD CO., L"),
                fact(1),
                preferred=("T- HANATOUR TD CO., L",),
            ),
        )

        self.assertEqual(
            tuple(channel.worksheet for channel in report.channels),
            STANDARD_SHEETS + ("T- HANATOUR TD CO., L",),
        )
        self.assertEqual(report.row_count, 2)
        self.assertEqual(report.channel_manifest[-1], ("T- HANATOUR TD CO., L", 6, 1))
        self.assertEqual(report.filename, "各渠道情况-2026年07月-更新至7.2.xlsx")

    def test_kb_payload_has_extra_typed_amount(self):
        report = build_monthly_report(
            2026,
            7,
            date(2026, 7, 2),
            snapshot(fact(1, channel="DY-AI-Easy-KB", rooms=2)),
        )

        payload = report.to_workbook_payload()
        kb = next(item for item in payload["channels"] if item["worksheet"] == "DY-AI-Easy-KB")
        self.assertEqual(kb["headers"][-1], KB_HEADER)
        self.assertEqual(kb["rows"][0][KB_HEADER], "200")
        self.assertEqual(kb["rows"][0]["TOTAL PRICE"], "1800")

    def test_empty_current_day_is_pinned(self):
        report = build_monthly_report(
            2026,
            7,
            date(2026, 7, 26),
            MonthlySnapshot(
                facts=(),
                daily_versions=(DailyVersionPin(date(2026, 7, 26), 126),),
                channel_observations=(),
            ),
        )
        self.assertEqual(report.row_count, 0)
        self.assertEqual(report.daily_versions[0].daily_version_id, 126)

    def test_nonempty_report_rejects_as_of_after_greatest_arrival(self):
        with self.assertRaises(MonthlyReportError) as caught:
            build_monthly_report(
                2026,
                7,
                date(2026, 7, 30),
                snapshot(fact(1, business_date=date(2026, 7, 27))),
            )

        self.assertEqual(caught.exception.code, ErrorCode.SOURCE_INVALID)
        self.assertIn("greatest included ARRIVAL", caught.exception.safe_message)

    def test_mismatched_metric_or_pin_is_rejected(self):
        source = fact(1)
        cases = (
            MonthlySnapshot(
                (source,),
                (DailyVersionPin(source.business_date, 999),),
                (ChannelObservation(source.business_date, 999, "QBD", 1, 1),),
            ),
            MonthlySnapshot(
                (source,),
                (DailyVersionPin(source.business_date, source.daily_version_id),),
                (ChannelObservation(source.business_date, source.daily_version_id, "QBD", 1, 2),),
            ),
        )
        for current in cases:
            with self.subTest(current=current):
                with self.assertRaises(MonthlyReportError) as caught:
                    build_monthly_report(2026, 7, date(2026, 7, 26), current)
                self.assertEqual(caught.exception.code, ErrorCode.SOURCE_INVALID)


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