from __future__ import annotations

import argparse
import contextlib
import copy
import hashlib
import io
import json
import tempfile
import unittest
from datetime import date, datetime, timezone
from pathlib import Path

from arr_ingestion.contracts import ArtifactRef, IngestionError
from arr_ingestion.direct_contracts import (
    DIRECT_CONTRACT_VERSION,
    MAX_DIRECT_PAYLOAD_BYTES,
    DirectSubmissionReceipt,
    DirectSubmissionRequest,
    ReceivedDirectSubmission,
    SubmissionGrant,
    VerifiedDirectSubmission,
)
from arr_ingestion.direct_service import DirectSubmissionService
from arr_ingestion.direct_validation import DirectResultValidator
from arr_processing.policy import load_legacy_direct_processor_policy
from tests.test_arr_ingestion_validation import MemoryStore, policy, processor
from tests.test_arr_opera_daily_ingest import reservation, success_xml, xml_document


GRANT = "G" * 43
JOB_ID = "arrjob-direct-001"
SOURCE_KEY = (
    "arr/jobs/arrjob-direct-001/attempts/0001/committed/"
    "source_xml/source.xml"
)
PROJECT_ROOT = Path(__file__).resolve().parents[1]


def legacy_policy():
    return load_legacy_direct_processor_policy(PROJECT_ROOT)


def run_direct_processor(
    root: Path,
) -> tuple[bytes, dict[str, object]]:
    source = root / "source.xml"
    output = root / "output"
    result = output / "result.json"
    structured = output / "structured-result.json"
    root.mkdir(parents=True, exist_ok=True)
    source.write_text(success_xml(), encoding="utf-8")
    with contextlib.redirect_stdout(io.StringIO()):
        exit_code = processor.process(
            argparse.Namespace(
                xml=str(source.resolve()),
                output_dir=str(output.resolve()),
                result_json=str(result.resolve()),
                structured_result_json=str(structured.resolve()),
                legacy_v3_output=True,
            )
        )
    if exit_code != 0:
        raise AssertionError(f"synthetic processor failed: {exit_code}")
    return source.read_bytes(), json.loads(structured.read_text(encoding="utf-8"))


def request_dict(payload: dict[str, object]) -> dict[str, object]:
    return {
        "submission_grant": GRANT,
        "job_id": JOB_ID,
        "attempt_no": 1,
        "payload": payload,
    }


def source_ref(source: bytes) -> ArtifactRef:
    return ArtifactRef.from_dict(
        "source_xml",
        {
            "object_key": SOURCE_KEY,
            "original_filename": "source.xml",
            "sha256": hashlib.sha256(source).hexdigest(),
            "byte_size": len(source),
            "mime_type": "application/xml",
        },
    )


def received(
    payload: dict[str, object],
    source: bytes,
    *,
    receipt: DirectSubmissionReceipt | None = None,
) -> ReceivedDirectSubmission:
    return ReceivedDirectSubmission(
        submission_id=17,
        status="committed" if receipt is not None else "validating",
        request=DirectSubmissionRequest.from_dict(request_dict(payload)),
        source=source_ref(source),
        receipt=receipt,
    )


def receipt() -> DirectSubmissionReceipt:
    return DirectSubmissionReceipt(
        status="committed",
        job_id=JOB_ID,
        attempt_no=1,
        business_date=date(2026, 7, 27),
        daily_version_id=91,
        version_no=3,
        record_count=5,
    )


class FakeRepository:
    def __init__(self, state: ReceivedDirectSubmission):
        self.state = state
        self.received_requests: list[DirectSubmissionRequest] = []
        self.rejections: list[tuple[int, str]] = []
        self.commits: list[VerifiedDirectSubmission] = []
        self.expiry_limits: list[int] = []
        self.ready_checks = 0

    def assert_ready(self) -> None:
        self.ready_checks += 1

    def issue_grant(
        self,
        job_id: str,
        attempt_no: int,
        *,
        ttl_seconds: int,
    ) -> SubmissionGrant:
        self.issued = (job_id, attempt_no, ttl_seconds)
        return SubmissionGrant(
            GRANT,
            job_id,
            attempt_no,
            datetime(2026, 7, 29, tzinfo=timezone.utc),
        )

    def receive(self, request: DirectSubmissionRequest) -> ReceivedDirectSubmission:
        self.received_requests.append(request)
        return self.state

    def reject(
        self,
        submission: ReceivedDirectSubmission,
        failure_code: str,
    ) -> None:
        self.rejections.append((submission.submission_id, failure_code))

    def commit(
        self,
        verified: VerifiedDirectSubmission,
    ) -> DirectSubmissionReceipt:
        self.commits.append(verified)
        return receipt()

    def expire_stale(self, *, limit: int) -> int:
        self.expiry_limits.append(limit)
        return 2


class FakeValidator:
    def __init__(self, error: IngestionError | None = None):
        self.error = error
        self.calls: list[ReceivedDirectSubmission] = []

    def validate(
        self,
        submission: ReceivedDirectSubmission,
    ) -> VerifiedDirectSubmission:
        self.calls.append(submission)
        if self.error is not None:
            raise self.error
        return VerifiedDirectSubmission(submission, "a" * 64)


class DirectContractTests(unittest.TestCase):
    def test_request_is_canonical_bounded_and_keeps_grant_out_of_repr(self) -> None:
        with tempfile.TemporaryDirectory() as temp_dir:
            _source, payload = run_direct_processor(Path(temp_dir))
        request = DirectSubmissionRequest.from_dict(request_dict(payload))
        self.assertEqual(request.business_date, date(2026, 7, 27))
        self.assertEqual(request.record_count, 5)
        self.assertEqual(
            request.payload_sha256,
            hashlib.sha256(request.payload_bytes).hexdigest(),
        )
        self.assertLess(len(request.payload_bytes), MAX_DIRECT_PAYLOAD_BYTES)
        self.assertNotIn(GRANT, repr(request))
        self.assertTrue(request.submission_key.startswith("direct:"))

    def test_request_rejects_extra_fields_nonfinite_json_and_oversize(self) -> None:
        minimal = {
            "status": "success",
            "activation_eligible": True,
            "business_date": "2026-07-27",
            "processor_version": "3.0.0",
            "rule_set_sha256": "a" * 64,
            "result_schema_version": "3.0",
            "records": [],
        }
        extra = request_dict(minimal)
        extra["payload_sha256"] = "a" * 64
        with self.assertRaises(IngestionError) as raised:
            DirectSubmissionRequest.from_dict(extra)
        self.assertEqual(raised.exception.code, "DIRECT_SUBMISSION_INVALID")

        nonfinite = copy.deepcopy(minimal)
        nonfinite["number"] = float("nan")
        with self.assertRaises(IngestionError) as raised:
            DirectSubmissionRequest.from_dict(request_dict(nonfinite))
        self.assertEqual(raised.exception.code, "DIRECT_SUBMISSION_INVALID")

        oversize = copy.deepcopy(minimal)
        oversize["padding"] = "x" * MAX_DIRECT_PAYLOAD_BYTES
        with self.assertRaises(IngestionError) as raised:
            DirectSubmissionRequest.from_dict(request_dict(oversize))
        self.assertEqual(raised.exception.code, "DIRECT_PAYLOAD_TOO_LARGE")

    def test_legacy_direct_projection_never_enters_manual_price_review(self) -> None:
        source_xml = xml_document(
            reservation(1, rate_code="GRPA1", rate_amount="999999")
        )
        with tempfile.TemporaryDirectory() as temp_dir:
            root = Path(temp_dir)
            source = root / "source.xml"
            output = root / "output"
            result = output / "result.json"
            structured = output / "structured-result.json"
            source.write_text(source_xml, encoding="utf-8")
            with contextlib.redirect_stdout(io.StringIO()):
                exit_code = processor.process(
                    argparse.Namespace(
                        xml=str(source.resolve()),
                        output_dir=str(output.resolve()),
                        result_json=str(result.resolve()),
                        structured_result_json=str(structured.resolve()),
                        legacy_v3_output=True,
                    )
                )
            result_payload = json.loads(result.read_text(encoding="utf-8"))
            structured_payload = json.loads(structured.read_text(encoding="utf-8"))
        self.assertEqual(exit_code, 2)
        self.assertEqual(result_payload["version"], "3.0")
        self.assertEqual(result_payload["status"], "failed")
        self.assertEqual(structured_payload["result_schema_version"], "3.0")
        self.assertEqual(structured_payload["status"], "failed")
        self.assertNotIn("review_required", json.dumps(structured_payload))

    def test_receipt_round_trip_is_minimal_and_strict(self) -> None:
        value = receipt()
        payload = value.to_dict()
        self.assertEqual(payload["contract_version"], DIRECT_CONTRACT_VERSION)
        self.assertEqual(DirectSubmissionReceipt.from_dict(payload), value)
        payload["guest_name"] = "must-not-be-accepted"
        with self.assertRaises(IngestionError):
            DirectSubmissionReceipt.from_dict(payload)


class DirectReplayValidationTests(unittest.TestCase):
    def test_source_is_replayed_and_daily_zip_hash_variation_is_normalized(self) -> None:
        with tempfile.TemporaryDirectory() as temp_dir:
            source, payload = run_direct_processor(Path(temp_dir) / "agent")
            state = received(payload, source)
            validator = DirectResultValidator(
                MemoryStore({SOURCE_KEY: source}),
                legacy_policy(),
            )
            verified = validator.validate(state)
        self.assertEqual(verified.submission.submission_id, 17)
        self.assertEqual(len(verified.replay_payload_sha256), 64)

    def test_business_fact_tamper_is_rejected_by_replay(self) -> None:
        with tempfile.TemporaryDirectory() as temp_dir:
            source, payload = run_direct_processor(Path(temp_dir) / "agent")
            tampered = copy.deepcopy(payload)
            retained = next(
                row
                for row in tampered["records"]
                if row["outcome"] == "retained"
            )
            retained["total_price"] += 1
            validator = DirectResultValidator(
                MemoryStore({SOURCE_KEY: source}),
                legacy_policy(),
            )
            with self.assertRaises(IngestionError) as raised:
                validator.validate(received(tampered, source))
        self.assertEqual(raised.exception.code, "DIRECT_RESULT_MISMATCH")


class DirectSubmissionServiceTests(unittest.TestCase):
    def setUp(self) -> None:
        self.temp = tempfile.TemporaryDirectory()
        self.source, self.payload = run_direct_processor(Path(self.temp.name))

    def tearDown(self) -> None:
        self.temp.cleanup()

    def test_commit_and_terminal_replay(self) -> None:
        state = received(self.payload, self.source)
        repository = FakeRepository(state)
        validator = FakeValidator()
        service = DirectSubmissionService(validator, repository)
        result = service.submit(request_dict(self.payload))
        self.assertEqual(result.status, "committed")
        self.assertEqual(len(validator.calls), 1)
        self.assertEqual(len(repository.commits), 1)

        terminal = received(self.payload, self.source, receipt=receipt())
        terminal_repository = FakeRepository(terminal)
        terminal_validator = FakeValidator()
        result = DirectSubmissionService(
            terminal_validator,
            terminal_repository,
        ).submit(request_dict(self.payload))
        self.assertEqual(result, receipt())
        self.assertEqual(terminal_validator.calls, [])

    def test_terminal_validation_error_rejects_and_retryable_error_stays_open(self) -> None:
        state = received(self.payload, self.source)
        repository = FakeRepository(state)
        service = DirectSubmissionService(
            FakeValidator(
                IngestionError(
                    "DIRECT_RESULT_MISMATCH",
                    "direct result does not match replay",
                )
            ),
            repository,
        )
        with self.assertRaises(IngestionError):
            service.submit(request_dict(self.payload))
        self.assertEqual(
            repository.rejections,
            [(17, "DIRECT_RESULT_MISMATCH")],
        )

        retry_repository = FakeRepository(state)
        retry_service = DirectSubmissionService(
            FakeValidator(
                IngestionError(
                    "DIRECT_REPLAY_UNAVAILABLE",
                    "replay unavailable",
                    retryable=True,
                )
            ),
            retry_repository,
        )
        with self.assertRaises(IngestionError):
            retry_service.submit(request_dict(self.payload))
        self.assertEqual(retry_repository.rejections, [])

    def test_grant_and_expiry_are_repository_owned(self) -> None:
        repository = FakeRepository(received(self.payload, self.source))
        service = DirectSubmissionService(FakeValidator(), repository)
        service.assert_ready()
        self.assertEqual(repository.ready_checks, 1)
        grant = service.issue_grant(JOB_ID, 1, ttl_seconds=600)
        self.assertEqual(grant.job_id, JOB_ID)
        self.assertEqual(repository.issued, (JOB_ID, 1, 600))
        self.assertEqual(service.expire_stale(limit=20), 2)
        self.assertEqual(repository.expiry_limits, [20])
        with self.assertRaises(IngestionError):
            service.expire_stale(limit=0)


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