from __future__ import annotations

import tempfile
import unittest
from datetime import date
from pathlib import Path

from arr_ingestion.contracts import IngestionError
from arr_ingestion.repository import InMemoryIngestionRepository
from arr_ingestion.service import IngestionService
from arr_ingestion.validation import DeliveryValidator
from arr_processing.local import LocalDailyProcessor
from arr_processing.policy import load_processor_policy
from arr_storage.filesystem import FilesystemObjectBackend
from arr_storage.store import ManagedObjectStore
from arr_web.contracts import PortalError
from arr_web.programmatic import ProgrammaticUploadCoordinator
from tests.test_arr_opera_daily_ingest import (
    reservation,
    success_xml,
    xml_document,
)


PROJECT_ROOT = Path(__file__).resolve().parents[1]


class RejectingManualReplayProcessor:
    def __init__(self, delegate: LocalDailyProcessor) -> None:
        self._delegate = delegate

    def run(self, *args, **kwargs):
        if kwargs.get("manual_overrides") is not None:
            raise IngestionError(
                "PROCESSOR_RESULT_INVALID",
                "manual replay contract is invalid",
            )
        return self._delegate.run(*args, **kwargs)


def coordinator(root: Path):
    store = ManagedObjectStore(
        FilesystemObjectBackend(root / "objects", create=True)
    )
    repository = InMemoryIngestionRepository()
    policy = load_processor_policy(PROJECT_ROOT)
    service = IngestionService(DeliveryValidator(store, policy), repository)
    return (
        ProgrammaticUploadCoordinator(
            object_store=store,
            ingestion_repository=repository,
            ingestion_service=service,
            processor=LocalDailyProcessor(policy),
            processor_version=policy.processor_version,
            rule_set_sha256=policy.rule_set_sha256,
        ),
        repository,
        root / "objects",
    )


class ProgrammaticUploadTests(unittest.TestCase):
    def test_valid_xml_reaches_atomic_terminal_success_without_remote_boundary(self):
        with tempfile.TemporaryDirectory() as temporary:
            upload, repository, objects = coordinator(Path(temporary))

            receipt = upload.submit("ARR.XML", success_xml().encode("utf-8"))

            self.assertEqual(receipt["status"], "succeeded")
            self.assertEqual(receipt["ingestion_status"], "committed")
            self.assertEqual(receipt["business_date"], "2026-07-27")
            self.assertEqual(receipt["version_no"], 1)
            self.assertEqual(
                repository._jobs[receipt["job_id"]].registration.uploaded_filename,
                "ARR.XML",
            )
            self.assertEqual(
                repository.current_version_id(date(2026, 7, 27)),
                receipt["daily_version_id"],
            )
            records = repository.version_records(int(receipt["daily_version_id"]))
            self.assertEqual(len(records), 5)
            self.assertEqual(
                sum(1 for item in records if item["outcome"] == "retained"),
                3,
            )
            committed = {
                path.name
                for path in objects.rglob("*")
                if path.is_file() and not path.name.endswith(".arr-metadata.json")
            }
            self.assertEqual(
                committed,
                {"source.xml", "result.json", "structured-result.json", "daily-report.xlsx"},
            )

    def test_price_unmatched_is_recorded_for_review_without_partial_active_version(self):
        price_review = xml_document(
            reservation(1, rate_code="GRPA1", rate_amount="900"),
            reservation(2, rate_code="GRPA1", rate_amount="1800"),
        )
        with tempfile.TemporaryDirectory() as temporary:
            upload, repository, objects = coordinator(Path(temporary))

            receipt = upload.submit("ARR.XML", price_review.encode("utf-8"))

            self.assertEqual(receipt["status"], "needs_review")
            self.assertEqual(receipt["ingestion_status"], "recorded_review")
            self.assertIsNotNone(receipt["review_case_id"])
            self.assertEqual(receipt["review_completed_items"], 0)
            self.assertEqual(receipt["review_total_items"], 1)
            self.assertIsNone(receipt["version_no"])
            self.assertIsNone(repository.current_version_id(date(2026, 7, 27)))
            committed = {
                path.name
                for path in objects.rglob("*")
                if path.is_file() and not path.name.endswith(".arr-metadata.json")
            }
            self.assertEqual(
                committed,
                {"source.xml", "result.json", "structured-result.json"},
            )

    def test_finalized_review_replays_original_xml_and_commits_manual_version(self):
        source = xml_document(
            reservation(1, rate_code="GRPA1", rate_amount="900"),
            reservation(2, rate_code="GRPA1", rate_amount="1800", rooms="2"),
        )
        with tempfile.TemporaryDirectory() as temporary:
            upload, repository, objects = coordinator(Path(temporary))
            receipt = upload.submit("ARR.XML", source.encode("utf-8"))
            self.assertEqual(receipt["status"], "needs_review")
            self.assertIsNone(repository.current_version_id(date(2026, 7, 27)))
            review = upload.get_price_review(receipt["job_id"], 50, 0)
            self.assertEqual(review["total_items"], 1)
            updated = upload.update_price_review_item(
                receipt["job_id"],
                review["items"][0]["item_id"],
                review["case_id"],
                review["revision"],
                "0",
                "operator@example.test",
            )
            final = upload.finalize_price_review(
                receipt["job_id"],
                updated["case_id"],
                updated["revision"],
                "operator@example.test",
            )

            self.assertEqual(final["status"], "succeeded")
            self.assertEqual(final["ingestion_status"], "committed")
            self.assertEqual(final["version_no"], 1)
            self.assertEqual(repository.current_version_id(date(2026, 7, 27)), final["daily_version_id"])
            records = repository.version_records(int(final["daily_version_id"]))
            self.assertEqual(
                [record["pricing_method"] for record in records],
                ["price_reference_exact", "manual_review"],
            )
            completed = upload.get_price_review(receipt["job_id"], 50, 0)
            self.assertEqual(completed["case_status"], "completed")
            committed = {
                path.name
                for path in objects.rglob("*")
                if path.is_file() and not path.name.endswith(".arr-metadata.json")
            }
            self.assertEqual(
                committed,
                {
                    "source.xml",
                    "result.json",
                    "structured-result.json",
                    "daily-report.xlsx",
                    "manual-override.json",
                },
            )

    def test_non_price_validation_error_remains_business_failure(self):
        invalid = xml_document(reservation(1, departure="2026-07-26"))
        with tempfile.TemporaryDirectory() as temporary:
            upload, repository, _objects = coordinator(Path(temporary))
            receipt = upload.submit("ARR.XML", invalid.encode("utf-8"))
            self.assertEqual(receipt["status"], "failed")
            self.assertEqual(receipt["ingestion_status"], "recorded_failure")
            self.assertIsNone(repository.current_version_id(date(2026, 7, 27)))

    def test_deterministic_finalization_error_closes_review_case(self):
        source = xml_document(
            reservation(1, rate_code="GRPA1", rate_amount="900"),
            reservation(2, rate_code="GRPA1", rate_amount="1800"),
        )
        with tempfile.TemporaryDirectory() as temporary:
            upload, repository, _objects = coordinator(Path(temporary))
            receipt = upload.submit("ARR.XML", source.encode("utf-8"))
            review = upload.get_price_review(receipt["job_id"], 50, 0)
            updated = upload.update_price_review_item(
                receipt["job_id"],
                review["items"][0]["item_id"],
                review["case_id"],
                review["revision"],
                "0",
                "operator@example.test",
            )
            upload.processor = RejectingManualReplayProcessor(upload.processor)  # type: ignore[assignment]
            with self.assertRaises(PortalError) as raised:
                upload.finalize_price_review(
                    receipt["job_id"],
                    updated["case_id"],
                    updated["revision"],
                    "operator@example.test",
                )

            self.assertEqual(raised.exception.code, "PROCESSOR_RESULT_INVALID")
            self.assertEqual(
                upload.get_price_review(receipt["job_id"], 50, 0)["case_status"],
                "failed",
            )
            self.assertEqual(repository._jobs[receipt["job_id"]].status, "failed")
            self.assertIsNone(repository.current_version_id(date(2026, 7, 27)))


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