from __future__ import annotations

import hashlib
import json
import tempfile
import unittest
from dataclasses import replace
from datetime import date
from pathlib import Path

from arr_ingestion.contracts import DeliveryEnvelope, IngestionError
from arr_ingestion.repository import (
    InMemoryIngestionRepository,
    JobRegistration,
)
from arr_ingestion.service import IngestionService
from arr_ingestion.validation import DeliveryValidator
from tests.test_arr_ingestion_validation import build_delivery, policy
from tests.test_arr_opera_daily_ingest import reservation, xml_document


def register(repository: InMemoryIngestionRepository, raw: bytes) -> DeliveryEnvelope:
    envelope = DeliveryEnvelope.from_dict(json.loads(raw))
    source = envelope.artifacts["source_xml"]
    assert source is not None
    repository.register_job(
        JobRegistration(
            job_id=envelope.job_id,
            source=source,
            processor_version=envelope.processor_version,
            rule_set_sha256=envelope.rule_set_sha256,
            attempt_no=envelope.attempt_no,
            idempotency_key=hashlib.sha256(
                f"{envelope.job_id}:{envelope.attempt_no}".encode("utf-8")
            ).hexdigest(),
        )
    )
    return envelope


class IngestionServiceTests(unittest.TestCase):
    def success_xml(self, room: str = "SYN-ROOM-1") -> str:
        return xml_document(
            reservation(1, room=room, res_comment="SYN-GROUP", departure="2026-07-30")
        )

    def test_success_commits_all_records_and_same_callback_is_idempotent(self):
        with tempfile.TemporaryDirectory() as temp_dir:
            raw, store, _values = build_delivery(self.success_xml(), Path(temp_dir))
            repository = InMemoryIngestionRepository()
            register(repository, raw)
            service = IngestionService(DeliveryValidator(store, policy()), repository)

            first = service.ingest(raw)
            second = service.ingest(raw)
            self.assertEqual(first.status, "committed")
            self.assertEqual(second, first)
            self.assertEqual(first.version_no, 1)
            self.assertEqual(repository.current_version_id(date(2026, 7, 27)), first.daily_version_id)
            self.assertEqual(len(repository.version_records(first.daily_version_id or 0)), 1)

    def test_same_delivery_id_with_different_envelope_bytes_conflicts(self):
        with tempfile.TemporaryDirectory() as temp_dir:
            raw, store, _values = build_delivery(self.success_xml(), Path(temp_dir))
            repository = InMemoryIngestionRepository()
            register(repository, raw)
            service = IngestionService(DeliveryValidator(store, policy()), repository)
            service.ingest(raw)
            rewritten = json.dumps(json.loads(raw), ensure_ascii=False, indent=2).encode("utf-8")
            with self.assertRaisesRegex(IngestionError, "identifier conflicts") as raised:
                service.ingest(rewritten)
            self.assertEqual(raised.exception.code, "CALLBACK_CONFLICT")

    def test_same_source_and_rule_from_new_job_reuses_committed_version(self):
        with tempfile.TemporaryDirectory() as temp_dir:
            root = Path(temp_dir)
            raw, store, _values = build_delivery(self.success_xml(), root / "first")
            repository = InMemoryIngestionRepository()
            register(repository, raw)
            first = IngestionService(DeliveryValidator(store, policy()), repository).ingest(raw)

            envelope = json.loads(raw)
            envelope["delivery_id"] = "delivery-002"
            envelope["job_id"] = "job-002"
            second_raw = json.dumps(envelope, ensure_ascii=False).encode("utf-8")
            register(repository, second_raw)
            second = IngestionService(DeliveryValidator(store, policy()), repository).ingest(second_raw)

            self.assertEqual(second.status, "already_committed")
            self.assertEqual(second.daily_version_id, first.daily_version_id)
            self.assertEqual(second.version_no, first.version_no)

    def test_repository_identity_allows_same_source_under_new_rule(self):
        """A future allowlisted rule creates a new immutable version, not a no-op."""
        with tempfile.TemporaryDirectory() as temp_dir:
            raw, store, _values = build_delivery(self.success_xml(), Path(temp_dir))
            repository = InMemoryIngestionRepository()
            register(repository, raw)
            validator = DeliveryValidator(store, policy())
            verified = validator.validate(raw)
            first = repository.commit_delivery(verified)

            new_rule = "1" * 64
            new_envelope = replace(
                verified.envelope,
                delivery_id="delivery-new-rule",
                job_id="job-new-rule",
                rule_set_sha256=new_rule,
            )
            source = new_envelope.artifacts["source_xml"]
            assert source is not None
            repository.register_job(
                JobRegistration(
                    job_id=new_envelope.job_id,
                    source=source,
                    processor_version=new_envelope.processor_version,
                    rule_set_sha256=new_rule,
                    attempt_no=1,
                    idempotency_key=hashlib.sha256(b"job-new-rule:1").hexdigest(),
                )
            )
            new_verified = replace(
                verified,
                envelope=new_envelope,
                envelope_sha256=hashlib.sha256(b"synthetic-new-rule-envelope").hexdigest(),
            )
            second = repository.commit_delivery(new_verified)

            self.assertEqual(second.status, "committed")
            self.assertEqual(second.version_no, 2)
            self.assertNotEqual(second.daily_version_id, first.daily_version_id)
            self.assertEqual(repository.version_status(first.daily_version_id or 0), "superseded")

    def test_same_business_date_new_source_supersedes_only_after_success(self):
        with tempfile.TemporaryDirectory() as temp_dir:
            root = Path(temp_dir)
            raw_one, store_one, _ = build_delivery(
                self.success_xml("SYN-ROOM-1"), root / "one"
            )
            repository = InMemoryIngestionRepository()
            register(repository, raw_one)
            first = IngestionService(
                DeliveryValidator(store_one, policy()), repository
            ).ingest(raw_one)

            raw_two, store_two, _ = build_delivery(
                self.success_xml("SYN-ROOM-2"), root / "two"
            )
            envelope_two = json.loads(raw_two)
            envelope_two["delivery_id"] = "delivery-002"
            envelope_two["job_id"] = "job-002"
            raw_two = json.dumps(envelope_two, ensure_ascii=False).encode("utf-8")
            register(repository, raw_two)
            second = IngestionService(
                DeliveryValidator(store_two, policy()), repository
            ).ingest(raw_two)

            self.assertEqual(second.status, "committed")
            self.assertEqual(second.version_no, 2)
            self.assertEqual(repository.version_status(first.daily_version_id or 0), "superseded")
            self.assertEqual(repository.version_status(second.daily_version_id or 0), "active")
            self.assertEqual(
                repository.current_version_id(date(2026, 7, 27)),
                second.daily_version_id,
            )

    def test_business_failure_is_recorded_without_switching_current(self):
        failure_xml = xml_document(
            reservation(1, departure="2026-07-26", res_comment="SYN-FAIL")
        )
        with tempfile.TemporaryDirectory() as temp_dir:
            raw, store, _values = build_delivery(failure_xml, Path(temp_dir))
            repository = InMemoryIngestionRepository()
            register(repository, raw)
            outcome = IngestionService(
                DeliveryValidator(store, policy()), repository
            ).ingest(raw)
            self.assertEqual(outcome.status, "recorded_failure")
            self.assertIsNone(outcome.daily_version_id)
            self.assertIsNone(repository.current_version_id(date(2026, 7, 27)))

    def test_review_receipt_has_no_version_then_frozen_manual_replay_commits_once(self):
        source = xml_document(
            reservation(1, rate_code="GRPA1", rate_amount="900"),
            reservation(2, rate_code="GRPA1", rate_amount="1800"),
        )
        with tempfile.TemporaryDirectory() as temp_dir:
            root = Path(temp_dir)
            review_raw, review_store, _values = build_delivery(source, root / "review")
            repository = InMemoryIngestionRepository()
            envelope = register(repository, review_raw)
            service = IngestionService(DeliveryValidator(review_store, policy()), repository)
            review_outcome = service.ingest(review_raw)

            self.assertEqual(review_outcome.status, "recorded_review")
            self.assertIsNone(review_outcome.daily_version_id)
            self.assertIsNone(repository.current_version_id(date(2026, 7, 27)))
            case_id = review_outcome.review_case_id
            self.assertIsNotNone(case_id)
            current = repository.get_price_review(envelope.job_id, 50, 0)
            self.assertEqual(current["total_items"], 1)
            self.assertEqual(current["completed_items"], 0)

            updated = repository.update_price_review_item(
                envelope.job_id,
                current["items"][0]["item_id"],
                str(case_id),
                current["revision"],
                "0",
                "operator@example.test",
            )
            self.assertEqual(updated["items"][0]["real_price"], "0")
            plan = repository.begin_price_review_generation(
                envelope.job_id,
                str(case_id),
                updated["revision"],
                "operator@example.test",
                envelope.processor_version,
                envelope.rule_set_sha256,
                "1" * 64,
            )
            self.assertEqual(plan.status, "ready")
            self.assertEqual(plan.attempt_no, 2)
            self.assertEqual(plan.source, envelope.artifacts["source_xml"])
            self.assertIsNotNone(plan.manual_override_bytes)
            self.assertIsNotNone(plan.manual_override_sha256)
            with self.assertRaisesRegex(IngestionError, "已冻结"):
                repository.update_price_review_item(
                    envelope.job_id,
                    current["items"][0]["item_id"],
                    str(case_id),
                    updated["revision"],
                    "1",
                    "operator@example.test",
                )

            manifest = json.loads((plan.manual_override_bytes or b"{}").decode("utf-8"))
            self.assertEqual(manifest["overrides"][0]["real_price"], "0.00")
            final_raw, final_store, _final_values = build_delivery(
                source,
                root / "final",
                manual_override=manifest,
                review_job_id=envelope.job_id,
                review_case_id=str(case_id),
            )
            final_envelope = json.loads(final_raw)
            final_envelope["delivery_id"] = "delivery-002"
            final_envelope["attempt_no"] = int(plan.attempt_no or 0)
            final_raw = json.dumps(final_envelope, ensure_ascii=False).encode("utf-8")
            final_service = IngestionService(DeliveryValidator(final_store, policy()), repository)
            final = final_service.ingest(final_raw)
            duplicate = final_service.ingest(final_raw)

            self.assertEqual(final.status, "committed")
            self.assertEqual(duplicate, final)
            self.assertEqual(final.version_no, 1)
            self.assertEqual(repository.current_version_id(date(2026, 7, 27)), final.daily_version_id)
            self.assertEqual(
                [record["pricing_method"] for record in repository.version_records(final.daily_version_id or 0)],
                ["price_reference_exact", "manual_review"],
            )
            completed = repository.get_price_review(envelope.job_id, 50, 0)
            self.assertEqual(completed["case_status"], "completed")

    def test_frozen_review_generation_failure_reuses_manifest_on_retry(self):
        source = xml_document(reservation(1, rate_code="GRPA1", rate_amount="1800"))
        with tempfile.TemporaryDirectory() as temp_dir:
            raw, store, _values = build_delivery(source, Path(temp_dir) / "review")
            repository = InMemoryIngestionRepository()
            envelope = register(repository, raw)
            outcome = IngestionService(DeliveryValidator(store, policy()), repository).ingest(raw)
            review = repository.get_price_review(envelope.job_id, 50, 0)
            updated = repository.update_price_review_item(
                envelope.job_id,
                review["items"][0]["item_id"],
                str(outcome.review_case_id),
                review["revision"],
                "12",
                "operator@example.test",
            )
            first = repository.begin_price_review_generation(
                envelope.job_id,
                str(outcome.review_case_id),
                updated["revision"],
                "operator@example.test",
                envelope.processor_version,
                envelope.rule_set_sha256,
                "2" * 64,
            )
            repository.record_price_review_generation_failure(
                envelope.job_id,
                str(outcome.review_case_id),
                int(first.attempt_no or 0),
                "OBJECT_STORE_UNAVAILABLE",
            )
            retry = repository.begin_price_review_generation(
                envelope.job_id,
                str(outcome.review_case_id),
                updated["revision"],
                "operator@example.test",
                envelope.processor_version,
                envelope.rule_set_sha256,
                "3" * 64,
            )
            self.assertEqual(retry.status, "ready")
            self.assertGreater(int(retry.attempt_no or 0), int(first.attempt_no or 0))
            self.assertEqual(retry.manual_override_bytes, first.manual_override_bytes)
            self.assertEqual(retry.manual_override_sha256, first.manual_override_sha256)

    def test_unknown_job_is_rejected_after_artifact_validation(self):
        with tempfile.TemporaryDirectory() as temp_dir:
            raw, store, _values = build_delivery(self.success_xml(), Path(temp_dir))
            service = IngestionService(
                DeliveryValidator(store, policy()), InMemoryIngestionRepository()
            )
            with self.assertRaisesRegex(IngestionError, "not registered") as raised:
                service.ingest(raw)
            self.assertEqual(raised.exception.code, "JOB_NOT_FOUND")


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