from __future__ import annotations

import argparse
import contextlib
import hashlib
import io
import json
import tempfile
import unittest
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
from typing import Any

from arr_ingestion.contracts import XLSX_MIME
from arr_ingestion.direct_contracts import SubmissionGrant
from arr_ingestion.repository import InMemoryIngestionRepository
from arr_ingestion.service import IngestionService
from arr_ingestion.validation import DeliveryValidator
from arr_processing.callbacks import AgentResultWriteback
from arr_processing.contracts import ProcessingRequest, ProcessingResult
from arr_processing.ledger import InMemoryProcessingLedger
from arr_processing.policy import load_legacy_direct_processor_policy
from arr_processing.registration import ProcessingOutputRegistrar
from arr_processing.runner import ProcessingRunner, RemoteRunSnapshot
from arr_processing.signatures import SignedResultCodec
from arr_storage.filesystem import FilesystemObjectBackend
from arr_storage.contracts import ObjectAddress
from arr_storage.store import ManagedObjectStore
from arr_web.legacy_agent_services import ObjectStoreUploadCoordinator
from tests.test_arr_ingestion_validation import policy
from tests.test_arr_opera_daily_ingest import (
    core,
    success_xml,
)


NOW = datetime(2026, 7, 28, 9, 0, tzinfo=timezone.utc)
SIGNING_KEY = b"fake-superagent-signing-key-at-least-32-bytes"


class CapturingRepository:
    def __init__(self) -> None:
        self.inner = InMemoryIngestionRepository()
        self.registration: Any = None

    def register_job(self, registration: Any) -> None:
        self.registration = registration
        self.inner.register_job(registration)

    def issue_grant(
        self,
        job_id: str,
        attempt_no: int,
        *,
        ttl_seconds: int,
    ) -> SubmissionGrant:
        return SubmissionGrant(
            "G" * 43,
            job_id,
            attempt_no,
            NOW + timedelta(seconds=ttl_seconds),
        )

    def commit_delivery(self, delivery: Any) -> Any:
        return self.inner.commit_delivery(delivery)


class FakeSuperAgentTransport:
    """Fetches one opaque source handle, invokes the real Skill, and signs handles."""

    def __init__(
        self,
        root: Path,
        store: ManagedObjectStore,
        codec: SignedResultCodec,
    ) -> None:
        self.root = root
        self.store = store
        self.codec = codec
        self.remote_files: dict[str, bytes] = {}
        self.signed_result: str | None = None
        self.request: ProcessingRequest | None = None
        self.run_id = "run-fake-superagent-001"

    @staticmethod
    def _handle(role: str, filename: str, value: bytes, mime_type: str) -> dict[str, object]:
        return {
            "file_handle": f"remote-{role}-001",
            "original_filename": filename,
            "sha256": hashlib.sha256(value).hexdigest(),
            "byte_size": len(value),
            "mime_type": mime_type,
        }

    def submit(self, request: ProcessingRequest, idempotency_key: str) -> RemoteRunSnapshot:
        self.request = request
        fetched = self.root / "agent" / "source.xml"
        fetched.parent.mkdir(parents=True, exist_ok=True)
        self.store.materialize(
            self.store.key_policy.build(
                ObjectAddress(
                    request.job_id,
                    request.attempt_no,
                    "committed",
                    "source_xml",
                )
            ),
            fetched,
            25 * 1024 * 1024,
        )
        output_dir = self.root / "agent" / "skill-run" / "output"
        result_path = output_dir / "result.json"
        structured_path = output_dir / "structured-result.json"
        arguments = argparse.Namespace(
            xml=str(fetched.resolve()),
            output_dir=str(output_dir.resolve()),
            result_json=str(result_path.resolve()),
            structured_result_json=str(structured_path.resolve()),
            legacy_v3_output=True,
        )
        with contextlib.redirect_stdout(io.StringIO()):
            exit_code = core.process(arguments)
        result = json.loads(result_path.read_text(encoding="utf-8"))
        if exit_code != 0:
            raise AssertionError("synthetic success fixture failed in the deterministic Skill")
        values = {
            "daily_report": (output_dir / result["outputs"]["daily_report"]).read_bytes(),
            "result_json": (output_dir / "result.json").read_bytes(),
            "structured_result_json": (output_dir / "structured-result.json").read_bytes(),
        }
        filenames = {
            "daily_report": result["outputs"]["daily_report"],
            "result_json": "result.json",
            "structured_result_json": "structured-result.json",
        }
        mime_types = {
            "daily_report": XLSX_MIME,
            "result_json": "application/json",
            "structured_result_json": "application/json",
        }
        artifacts = {
            role: self._handle(role, filenames[role], value, mime_types[role])
            for role, value in values.items()
        }
        artifacts["exception_report"] = None
        for role, value in values.items():
            self.remote_files[str(artifacts[role]["file_handle"])] = value
        payload = {
            "contract_version": "arr-opera-daily-result-1",
            "delivery_id": "delivery-" + request.job_id,
            "job_id": request.job_id,
            "attempt_no": request.attempt_no,
            "remote_run_id": self.run_id,
            "status": "success",
            "business_date": result["business_date"],
            "processor_version": request.processor_version,
            "rule_set_sha256": request.rule_set_sha256,
            "result_schema_version": "3.0",
            "artifacts": artifacts,
        }
        signed = self.codec.sign(
            ProcessingResult.from_dict(payload),
            issued_at=NOW,
            nonce="fake-superagent-nonce-001",
        )
        self.signed_result = signed.decode("utf-8")
        return RemoteRunSnapshot(self.run_id, "running")

    def get(self, request: ProcessingRequest, remote_run_id: str) -> RemoteRunSnapshot:
        if request != self.request or remote_run_id != self.run_id or self.signed_result is None:
            raise AssertionError("fake SuperAgent correlation failed")
        return RemoteRunSnapshot(self.run_id, "success", self.signed_result)

    def cancel(self, request: ProcessingRequest, remote_run_id: str) -> RemoteRunSnapshot:
        return RemoteRunSnapshot(remote_run_id, "cancelled")

    def materialize(self, file_handle: str, destination: Path, max_bytes: int) -> None:
        value = self.remote_files[file_handle]
        if len(value) > max_bytes:
            raise AssertionError("fake output exceeded its declared limit")
        destination.write_bytes(value)


class ArrFullVerticalSliceTests(unittest.TestCase):
    def test_xml_object_remote_skill_arr_validation_and_atomic_fact_commit(self) -> None:
        with tempfile.TemporaryDirectory() as temporary:
            root = Path(temporary)
            store = ManagedObjectStore(
                FilesystemObjectBackend(root / "private-objects", create=True)
            )
            codec = SignedResultCodec(
                {"fake-key-1": SIGNING_KEY},
                signing_key_id="fake-key-1",
                clock=lambda: NOW,
            )
            transport = FakeSuperAgentTransport(root, store, codec)
            runner = ProcessingRunner(
                transport,
                InMemoryProcessingLedger(),
                codec,
                sleep=lambda _seconds: None,
            )
            repository = CapturingRepository()
            legacy_policy = load_legacy_direct_processor_policy(
                Path(__file__).resolve().parents[1]
            )
            upload = ObjectStoreUploadCoordinator(
                object_store=store,
                ingestion_repository=repository,
                processing_starter=runner,
                processor_version=legacy_policy.processor_version,
                rule_set_sha256=legacy_policy.rule_set_sha256,
            )

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

            class SourceResolver:
                def source_for_attempt(self, job_id: str, attempt_no: int):
                    self_outer.assertEqual(job_id, receipt["job_id"])
                    self_outer.assertEqual(attempt_no, 1)
                    return repository.registration.source

            self_outer = self
            writeback = AgentResultWriteback(
                runner,
                SourceResolver(),
                ProcessingOutputRegistrar(transport, store),
                IngestionService(
                    DeliveryValidator(store, legacy_policy),
                    repository,
                ),
            )
            outcome = writeback.accept(str(transport.signed_result).encode("utf-8"))

            self.assertEqual(outcome.status, "committed")
            self.assertEqual(outcome.business_date, "2026-07-27")
            self.assertEqual(
                repository.inner.current_version_id(date.fromisoformat(outcome.business_date)),
                outcome.daily_version_id,
            )
            records = repository.inner.version_records(int(outcome.daily_version_id))
            self.assertEqual(len(records), 5)
            self.assertEqual(sum(1 for row in records if row["outcome"] == "retained"), 3)
            self.assertEqual(
                sum(int(row.get("no_of_rooms") or 0) for row in records if row["outcome"] == "retained"),
                4,
            )
            self.assertNotIn("object_key", transport.request.message())
            self.assertNotIn(str(root), transport.request.message())
            self.assertTrue(
                repository.registration.source.object_key.endswith(
                    "/committed/source_xml/source.xml"
                )
            )


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