from __future__ import annotations

import io
import json
import tempfile
import unittest
from contextlib import redirect_stdout
from datetime import date
from decimal import Decimal
from pathlib import Path
from typing import Dict, Optional
from unittest.mock import patch

from company_reports import cli
from company_reports.contracts import (
    BatchSnapshot,
    BookingRoomItem,
    DailyVersionPin,
    ErrorCode,
    FinanceFact,
)
from company_reports.publishing import (
    BuildError,
    BuiltWorkbook,
    PublicationOutcome,
)
from company_reports.repository import (
    FileMetadata,
    RepositoryError,
    ReservedReport,
)
from company_reports.service import (
    CompanyReportService,
    RunRequest,
    write_batch_result,
)


def synthetic_snapshot(include_invalid_hana: bool = False) -> BatchSnapshot:
    facts = [
        FinanceFact(
            daily_record_id=1,
            daily_version_id=100,
            business_date=date(2026, 7, 8),
            channel_key="QBD",
            company_key="QBD",
            company_name="SYN-COMPANY",
            block_code="SYN-BLOCK-A",
            group_code_key="SYN-GROUP-A",
            res_comment="SYN-GROUP-A",
            room_category_label="SYN-RM2",
            arrival=date(2026, 7, 8),
            departure=date(2026, 7, 10),
            nights=2,
            total_price=Decimal("900"),
            booking_source_match_status="matched",
        )
    ]
    if include_invalid_hana:
        facts.append(
            FinanceFact(
                daily_record_id=2,
                daily_version_id=100,
                business_date=date(2026, 7, 8),
                channel_key="HANA TOUR",
                company_key="HANA TOUR",
                company_name="SYN-COMPANY",
                block_code="SYN-BLOCK-H",
                group_code_key=None,
                res_comment=None,
                room_category_label="SYN-RM2",
                arrival=date(2026, 7, 8),
                departure=date(2026, 7, 10),
                nights=2,
                total_price=Decimal("900"),
                booking_source_match_status="missing_group_code",
            )
        )
    item = BookingRoomItem(
        group_code_key="SYN-GROUP-A",
        parse_version_id=700,
        segment_no=1,
        arrival=date(2026, 7, 8),
        departure=date(2026, 7, 10),
        nights=2,
        item_no=1,
        room_type_raw="SYN-SUP-TWN",
        quantity=1,
    )
    return BatchSnapshot(
        facts=tuple(facts),
        room_items=(item,),
        daily_versions=(DailyVersionPin(date(2026, 7, 8), 100),),
        group_parse_versions={"SYN-GROUP-A": 700},
    )


class FakeRepository:
    def __init__(
        self,
        snapshot: Optional[BatchSnapshot] = None,
        load_error: Optional[RepositoryError] = None,
    ):
        self.snapshot = snapshot or BatchSnapshot((), (), (), {})
        self.load_error = load_error
        self.next_version = 1
        self.reserved = []
        self.activated = []
        self.failed = []

    def load_snapshot(self, _year: int, _month: int, _as_of: date) -> BatchSnapshot:
        if self.load_error:
            raise self.load_error
        return self.snapshot

    def reserve_report(self, report):
        reservation = ReservedReport(
            1000 + self.next_version,
            self.next_version,
            report.company,
        )
        self.next_version += 1
        self.reserved.append(reservation)
        return reservation

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

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


class FakeBuilder:
    def __init__(self, failing_company: Optional[str] = None):
        self.failing_company = failing_company
        self.companies = []
        self.reports = []

    def build(self, report, work_dir: Path) -> BuiltWorkbook:
        self.companies.append(report.company)
        self.reports.append(report)
        if report.company == self.failing_company:
            raise BuildError(
                ErrorCode.OUTPUT_VALIDATION_FAILED,
                "synthetic builder rejected output",
            )
        path = work_dir / report.filename
        path.write_bytes(("synthetic-workbook-" + report.company).encode("utf-8"))
        return BuiltWorkbook(path, "a" * 64, path.stat().st_size, {})


class FakePublisher:
    def __init__(self):
        self.companies = []

    def publish(self, report, reservation, built, repository, work_dir):
        self.companies.append(report.company)
        artifact = FileMetadata(
            "company_ten_day_xlsx",
            report.filename,
            f"outputs/company_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",
            f"{report.company}.result.json",
            f"outputs/company_reports/2026/07/archive/v{reservation.version_no:04d}/{report.company}.result.json",
            "b" * 64,
            1,
            "application/json",
        )
        repository.activate_report(reservation, artifact, result_json)
        return PublicationOutcome(
            current_path=work_dir / report.filename,
            archive_path=work_dir / report.filename,
            result_path=work_dir / "result.json",
            artifact=artifact,
            result_json=result_json,
        )


class CompanyReportServiceTests(unittest.TestCase):
    def test_missing_and_unmatched_group_codes_succeed_with_blank_booking_room(self):
        source_snapshot = synthetic_snapshot(include_invalid_hana=True)
        repository = FakeRepository(
            BatchSnapshot(
                facts=source_snapshot.facts,
                room_items=(),
                daily_versions=source_snapshot.daily_versions,
                group_parse_versions={},
            )
        )
        builder = FakeBuilder()
        publisher = FakePublisher()
        with tempfile.TemporaryDirectory(prefix="company-report-service-test-") as temp_dir:
            service = CompanyReportService(
                repository,
                builder,
                publisher,
                Path(temp_dir) / "staging",
            )
            result = service.run(
                RunRequest(
                    2026,
                    7,
                    date(2026, 7, 10),
                    ("QBD", "HanaTour"),
                )
            )

        self.assertEqual(result.status, "success")
        self.assertEqual(result.exit_code, 0)
        self.assertEqual([item.status for item in result.companies], ["success", "success"])
        self.assertEqual(builder.companies, ["QBD", "HanaTour"])
        self.assertEqual(publisher.companies, ["QBD", "HanaTour"])
        qbd_report, hana_report = builder.reports
        self.assertEqual(qbd_report.row_count, 1)
        self.assertEqual(qbd_report.periods[0].rows[0].res_comment, "SYN-GROUP-A")
        self.assertEqual(qbd_report.periods[0].rows[0].booking_room, "")
        self.assertEqual(hana_report.row_count, 1)
        self.assertEqual(hana_report.periods[0].rows[0].res_comment, "")
        self.assertEqual(hana_report.periods[0].rows[0].booking_room, "")
        self.assertEqual(result.companies[0].errors, ())
        self.assertEqual(result.companies[1].errors, ())
        serialized = json.dumps(result.to_dict(), ensure_ascii=False)
        self.assertNotIn("SYN-GROUP-A", serialized)
        self.assertNotIn("SYN-BLOCK", serialized)

    def test_builder_failure_marks_only_its_reserved_company_failed(self):
        repository = FakeRepository(synthetic_snapshot())
        builder = FakeBuilder(failing_company="QBD")
        publisher = FakePublisher()
        with tempfile.TemporaryDirectory(prefix="company-report-service-test-") as temp_dir:
            service = CompanyReportService(
                repository,
                builder,
                publisher,
                Path(temp_dir) / "staging",
            )
            result = service.run(
                RunRequest(
                    2026,
                    7,
                    date(2026, 7, 10),
                    ("QBD", "HanaTour"),
                )
            )

        self.assertEqual(result.status, "partial_failure")
        self.assertEqual([item.status for item in result.companies], ["failed", "success"])
        self.assertEqual(result.companies[0].errors[0]["code"], ErrorCode.OUTPUT_VALIDATION_FAILED)
        self.assertEqual(repository.failed[0][1], ErrorCode.OUTPUT_VALIDATION_FAILED)
        self.assertEqual(publisher.companies, ["HanaTour"])

    def test_source_failure_returns_all_company_failures_with_contract_exit_code(self):
        cases = (
            (ErrorCode.SOURCE_VERSION_MISSING, 2),
            (ErrorCode.INTERNAL_ERROR, 4),
        )
        for error_code, expected_exit in cases:
            with self.subTest(error_code=error_code):
                repository = FakeRepository(
                    load_error=RepositoryError(error_code, "synthetic source failure")
                )
                with tempfile.TemporaryDirectory(
                    prefix="company-report-service-test-"
                ) as temp_dir:
                    service = CompanyReportService(
                        repository,
                        FakeBuilder(),
                        FakePublisher(),
                        Path(temp_dir) / "staging",
                    )
                    result = service.run(
                        RunRequest(
                            2026,
                            7,
                            date(2026, 7, 10),
                            ("QBD", "HanaTour"),
                        )
                    )
                self.assertEqual(result.status, "failed")
                self.assertEqual(result.exit_code, expected_exit)
                self.assertTrue(
                    all(item.errors[0]["code"] == error_code for item in result.companies)
                )

    def test_batch_result_is_private_atomic_and_contains_no_source_values(self):
        repository = FakeRepository(synthetic_snapshot())
        with tempfile.TemporaryDirectory(prefix="company-report-service-test-") as temp_dir:
            root = Path(temp_dir)
            service = CompanyReportService(
                repository,
                FakeBuilder(),
                FakePublisher(),
                root / "staging",
            )
            result = service.run(
                RunRequest(2026, 7, date(2026, 7, 10), ("QBD",))
            )
            result_path = root / "results" / "batch.result.json"
            write_batch_result(result_path, result)
            text = result_path.read_text(encoding="utf-8")

            self.assertEqual(result_path.stat().st_mode & 0o777, 0o600)
            self.assertEqual(json.loads(text)["status"], "success")
            self.assertNotIn("SYN-GROUP-A", text)
            self.assertNotIn("SYN-BLOCK", text)

    def test_cli_request_and_missing_configuration_use_stable_exit_three(self):
        cases = (
            ["generate", "--month", "bad", "--as-of", "2026-07-10"],
            ["generate", "--month", "2026-07", "--as-of", "2026-07-09"],
            ["generate", "--month", "2026-07", "--as-of", "2026-07-10"],
        )
        for arguments in cases:
            with self.subTest(arguments=arguments), patch.dict("os.environ", {}, clear=True):
                output = io.StringIO()
                with redirect_stdout(output):
                    exit_code = cli.main(arguments)
                payload = json.loads(output.getvalue())
                self.assertEqual(exit_code, 3)
                self.assertEqual(payload["status"], "failed")
                self.assertEqual(
                    payload["errors"][0]["code"], ErrorCode.REQUEST_INVALID
                )


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