from __future__ import annotations

import json
import tempfile
import unittest
from datetime import date
from decimal import Decimal
from pathlib import Path

from monthly_reports.contracts import (
    ChannelObservation,
    DailyVersionPin,
    ErrorCode,
    MonthlyFact,
    MonthlySnapshot,
)
from monthly_reports.publishing import BuildError, BuiltWorkbook, PublicationOutcome
from monthly_reports.repository import FileMetadata, RepositoryError, ReservedReport
from monthly_reports.service import MonthlyReportService, RunRequest, write_run_result


def synthetic_snapshot() -> MonthlySnapshot:
    source = MonthlyFact(
        daily_record_id=1,
        daily_version_id=100,
        business_date=date(2026, 7, 8),
        channel_key="QBD",
        arrival=date(2026, 7, 8),
        departure=date(2026, 7, 10),
        nights=2,
        adults=2,
        children=0,
        block_code="SYN-BLOCK-A",
        no_of_rooms=1,
        company_name="SYN-COMPANY",
        confirmation_no="SYN-CONFIRMATION",
        disp_room_no="SYN-ROOM",
        effective_rate_amount=Decimal("450"),
        full_name="SYN-GUEST",
        res_comment="SYN-COMMENT",
        trace_text="",
        products="",
        rate_code="SYN-RATE",
        room_category_label="SYN-TYPE",
        real_price=Decimal("450"),
        total_price=Decimal("900"),
        kb_amount=None,
    )
    return MonthlySnapshot(
        (source,),
        (DailyVersionPin(date(2026, 7, 8), 100),),
        (ChannelObservation(date(2026, 7, 8), 100, "QBD", 1, 1),),
    )


class FakeRepository:
    def __init__(self, load_error=None):
        self.load_error = load_error
        self.failed = []
        self.activated = []

    def load_snapshot(self, _year, _month, _as_of):
        if self.load_error:
            raise self.load_error
        return synthetic_snapshot()

    def reserve_report(self, _report):
        return ReservedReport(44, 2)

    def activate_report(self, reservation, artifact, result_json, semantic_sha256):
        self.activated.append((reservation, artifact, result_json, semantic_sha256))

    def mark_failed(self, reservation, code, safe_message):
        self.failed.append((reservation, code, safe_message))


class FakeBuilder:
    def __init__(self, fail: bool = False):
        self.fail = fail

    def build(self, report, work_dir: Path):
        if self.fail:
            raise BuildError(
                ErrorCode.OUTPUT_VALIDATION_FAILED,
                "synthetic builder failure",
            )
        output = work_dir / report.filename
        output.write_bytes(b"synthetic-workbook")
        return BuiltWorkbook(
            output,
            "a" * 64,
            output.stat().st_size,
            {"semantic_sha256": "c" * 64},
        )


class FakePublisher:
    def publish(self, report, reservation, built, repository, work_dir):
        artifact = FileMetadata(
            "monthly_xlsx",
            report.filename,
            f"outputs/monthly_reports/2026/07/archive/v{reservation.version_no:04d}/{report.filename}",
            built.sha256,
            built.byte_size,
            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        )
        result_json = FileMetadata(
            "result_json",
            "result.json",
            f"outputs/monthly_reports/2026/07/archive/v{reservation.version_no:04d}/result.json",
            "b" * 64,
            1,
            "application/json",
        )
        repository.activate_report(
            reservation,
            artifact,
            result_json,
            str(built.summary["semantic_sha256"]),
        )
        return PublicationOutcome(
            latest_path=work_dir / "latest.xlsx",
            latest_result_path=work_dir / "latest.result.json",
            archive_path=work_dir / report.filename,
            result_path=work_dir / "result.json",
            artifact=artifact,
            result_json=result_json,
        )


class MonthlyReportsServiceTests(unittest.TestCase):
    def test_success_is_privacy_minimized_and_versioned(self):
        repository = FakeRepository()
        with tempfile.TemporaryDirectory(prefix="monthly-report-service-") as temp_dir:
            root = Path(temp_dir)
            service = MonthlyReportService(
                repository,
                FakeBuilder(),
                FakePublisher(),
                root / "staging",
            )
            result = service.run(RunRequest(2026, 7, date(2026, 7, 8)))
            result_path = root / "run.result.json"
            write_run_result(result_path, result)
            serialized = result_path.read_text(encoding="utf-8")

        self.assertEqual(result.status, "success")
        self.assertEqual(result.exit_code, 0)
        self.assertEqual(result.row_count, 1)
        self.assertEqual(result.report_version_id, 44)
        self.assertEqual(json.loads(serialized)["channel_manifest"][2]["worksheet"], "QBD")
        self.assertNotIn("SYN-GUEST", serialized)
        self.assertNotIn("SYN-CONFIRMATION", serialized)
        self.assertNotIn("SYN-BLOCK", serialized)
        self.assertEqual(len(repository.activated), 1)

    def test_published_snapshot_replay_reuses_registered_artifact_without_building(self):
        class PublishedRepository(FakeRepository):
            def reserve_report(self, _report):
                return ReservedReport(
                    report_version_id=44,
                    version_no=2,
                    report_status="active",
                    published_artifact=FileMetadata(
                        "monthly_xlsx",
                        "各渠道情况-2026年07月-更新至7.8.xlsx",
                        "outputs/monthly_reports/2026/07/archive/v0002/report.xlsx",
                        "a" * 64,
                        100,
                        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
                    ),
                )

        class NeverBuilder:
            def build(self, _report, _work_dir):
                raise AssertionError("published replay must not rebuild the workbook")

        repository = PublishedRepository()
        with tempfile.TemporaryDirectory(prefix="monthly-report-service-") as temp_dir:
            result = MonthlyReportService(
                repository,
                NeverBuilder(),
                FakePublisher(),
                Path(temp_dir) / "staging",
            ).run(RunRequest(2026, 7, date(2026, 7, 8)))

        self.assertEqual(result.status, "success")
        self.assertEqual(result.report_version_id, 44)
        self.assertEqual(result.artifact["sha256"], "a" * 64)
        self.assertEqual(repository.activated, [])

    def test_builder_failure_marks_reserved_report_failed(self):
        repository = FakeRepository()
        with tempfile.TemporaryDirectory(prefix="monthly-report-service-") as temp_dir:
            result = MonthlyReportService(
                repository,
                FakeBuilder(fail=True),
                FakePublisher(),
                Path(temp_dir) / "staging",
            ).run(RunRequest(2026, 7, date(2026, 7, 8)))

        self.assertEqual(result.status, "failed")
        self.assertEqual(result.error_code, ErrorCode.OUTPUT_VALIDATION_FAILED)
        self.assertEqual(repository.failed[0][0], ReservedReport(44, 2))

    def test_source_failure_uses_business_or_system_exit_code(self):
        cases = (
            (ErrorCode.SOURCE_SNAPSHOT_STALE, 2),
            (ErrorCode.DATABASE_FAILED, 4),
        )
        for code, exit_code in cases:
            with self.subTest(code=code), tempfile.TemporaryDirectory() as temp_dir:
                repository = FakeRepository(RepositoryError(code, "synthetic source failure"))
                result = MonthlyReportService(
                    repository,
                    FakeBuilder(),
                    FakePublisher(),
                    Path(temp_dir) / "staging",
                ).run(RunRequest(2026, 7, date(2026, 7, 8)))
                self.assertEqual(result.exit_code, exit_code)
                self.assertEqual(result.error_stage, "source")


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