from __future__ import annotations

from datetime import UTC, datetime
import unittest

from govoplan_core.core.modules import ModuleManifest
from govoplan_core.core.provider_governance import (
    ArchitectureDeclarationError,
    ExternalProviderDeclaration,
    ExternalProviderRuntimeState,
    ExternalProviderStateContext,
    ExternalProviderStateProviderRegistration,
    ModuleArchitectureDeclaration,
    ModuleArchitectureDocumentation,
    ModuleMaturityEvidence,
    ProviderBehaviorDeclaration,
    ProviderObjectDeclaration,
    collect_external_provider_states,
    declared_module_architecture,
    external_provider_from_mapping,
    module_architecture_from_mapping,
    module_architecture_issues,
)
from govoplan_core.core.registry import PlatformRegistry, RegistryError


def _architecture(
    *,
    providers: tuple[str, ...] = (),
    modes: tuple[str, ...] = ("external_mirror",),
) -> ModuleArchitectureDeclaration:
    return ModuleArchitectureDeclaration(
        layer="data_reporting_integration",
        kind="integration",
        maturity="vertical_slice",
        evidence=(
            ModuleMaturityEvidence(
                kind="test",
                reference="tests/test_provider.py",
                summary="Exercises the bounded provider contract.",
            ),
            ModuleMaturityEvidence(
                kind="documentation",
                reference="docs/PROVIDER.md",
                summary="Documents authority and outage behavior.",
            ),
        ),
        known_limits=("Only bounded test objects are supported.",),
        supported_authority_modes=modes,
        owned_concepts=("external transport",),
        non_owned_concepts=("domain records",),
        target_tested_providers=providers,
    )


def _mirror_provider() -> ExternalProviderDeclaration:
    return ExternalProviderDeclaration(
        id="connectors.read_mirror",
        module_id="connectors",
        label="Read-only mirror",
        maturity="read",
        operations=("discover", "search", "read", "preview"),
        objects=(
            ProviderObjectDeclaration(
                object_type="register_entry",
                field_groups=("identity", "content", "source_metadata"),
                authority_modes=("external_authoritative", "external_mirror"),
                default_authority_mode="external_mirror",
            ),
        ),
        behavior=ProviderBehaviorDeclaration(
            revision_tokens="ETag and source revision are retained.",
            concurrency="Reads can require an expected source revision.",
            freshness="The latest successful acquisition time is reported.",
            health="Transport and parsing health are reported separately.",
            max_read_items=500,
            evidence="Immutable snapshots retain acquisition provenance.",
            correction="Refresh creates a new snapshot; old evidence is retained.",
            reconciliation="Compare the latest source revision and content digest.",
            outage="The last governed snapshot remains readable as stale.",
            classifications=("internal",),
            purposes=("governed import",),
            retention="Owning datasource retention policy applies.",
            secret_handling="Credentials stay in credential envelopes.",
        ),
        capability_names=("connectors.readMirror",),
    )


def _writable_provider() -> ExternalProviderDeclaration:
    return ExternalProviderDeclaration(
        id="calendar.caldav_sync",
        module_id="calendar",
        label="CalDAV synchronization",
        maturity="synchronize",
        operations=("discover", "read", "write", "delete", "synchronize", "preview"),
        objects=(
            ProviderObjectDeclaration(
                object_type="calendar_event",
                field_groups=("identity", "schedule", "participants", "recurrence"),
                authority_modes=("external_authoritative", "governed_sync"),
                default_authority_mode="governed_sync",
            ),
        ),
        behavior=ProviderBehaviorDeclaration(
            revision_tokens="ETag and sync token.",
            concurrency="Conditional requests reject stale ETags.",
            freshness="Last successful sync and pending changes are reported.",
            health="Discovery, authentication, and collection health are separate.",
            max_read_items=1000,
            idempotency="Stable UID and operation keys suppress duplicate effects.",
            retry="Bounded retry is limited to classified transient failures.",
            timeout_seconds=30,
            conflicts="Conflicts remain explicit until policy or a user resolves them.",
            outcome_unknown="Timed-out writes are outcome-unknown, never blindly retried.",
            outcome_unknown_supported=True,
            evidence="Request, response token, and reconciliation evidence are retained.",
            audit_event_types=("calendar.sync.requested", "calendar.sync.reconciled"),
            correction="A later governed write or tombstone corrects remote state.",
            rollback="Remote writes are not assumed rollback-safe.",
            compensation="A compensating update can be requested after reconciliation.",
            reconciliation="Read by UID and compare ETag before retrying.",
            outage="Local state remains marked stale and pending effects stay queued.",
            classifications=("confidential",),
            purposes=("calendar synchronization",),
            retention="Calendar and audit retention policies apply independently.",
            secret_handling="Credentials are referenced through credential envelopes.",
        ),
        capability_names=("calendar.sync",),
    )


def _state_registration(
    *,
    module_id: str,
    provider_id: str,
) -> ExternalProviderStateProviderRegistration:
    return ExternalProviderStateProviderRegistration(
        module_id=module_id,
        provider_id=provider_id,
        provider=lambda _context: (),
    )


class ProviderGovernanceContractTests(unittest.TestCase):
    def test_concise_declaration_keeps_repository_evidence_explicit(self) -> None:
        architecture = declared_module_architecture(
            layer="human_work_procedure",
            kind="domain",
            maturity="vertical_slice",
            documentation_ref="docs/DOMAIN.md",
            test_ref="tests/test_service.py",
            known_limits=("Only the bounded service slice is implemented.",),
            owned_concepts=("domain record",),
            non_owned_concepts=("identity",),
        )

        self.assertEqual("vertical_slice", architecture.maturity)
        self.assertEqual(
            {"documentation", "test"},
            {item.kind for item in architecture.evidence},
        )
        self.assertEqual(("domain record",), architecture.owned_concepts)

    def test_declarations_round_trip_through_catalog_mappings(self) -> None:
        provider = _writable_provider()
        architecture = _architecture(
            providers=(provider.id,),
            modes=("external_authoritative", "governed_sync"),
        )

        self.assertEqual(
            architecture,
            module_architecture_from_mapping(architecture.to_dict()),
        )
        self.assertEqual(
            provider,
            external_provider_from_mapping(provider.to_dict()),
        )

    def test_read_only_mirror_declaration_is_registry_validated(self) -> None:
        provider = _mirror_provider()
        registry = PlatformRegistry()
        registry.register(
            ModuleManifest(
                id="connectors",
                name="Connectors",
                version="1.0.0",
                architecture=_architecture(
                    providers=(provider.id,),
                    modes=("external_authoritative", "external_mirror"),
                ),
                external_providers=(provider,),
                external_provider_state_providers=(
                    _state_registration(
                        module_id="connectors",
                        provider_id=provider.id,
                    ),
                ),
                capability_factories={"connectors.readMirror": lambda _context: object()},
            )
        )

        snapshot = registry.validate()

        self.assertEqual("connectors", snapshot.manifests[0].id)
        self.assertEqual((provider,), registry.external_provider_declarations())

    def test_writable_outcome_unknown_provider_is_explicit(self) -> None:
        provider = _writable_provider()
        registry = PlatformRegistry()
        registry.register(
            ModuleManifest(
                id="calendar",
                name="Calendar",
                version="1.0.0",
                architecture=_architecture(
                    providers=(provider.id,),
                    modes=("external_authoritative", "governed_sync"),
                ),
                external_providers=(provider,),
                external_provider_state_providers=(
                    _state_registration(
                        module_id="calendar",
                        provider_id=provider.id,
                    ),
                ),
                capability_factories={"calendar.sync": lambda _context: object()},
            )
        )

        registry.validate()

        payload = provider.to_dict()
        self.assertTrue(payload["behavior"]["outcome_unknown_supported"])
        self.assertIn("reconciliation", payload["behavior"])

    def test_provider_requires_a_sanitized_runtime_state_registration(self) -> None:
        provider = _mirror_provider()
        registry = PlatformRegistry()
        registry.register(
            ModuleManifest(
                id="connectors",
                name="Connectors",
                version="1.0.0",
                architecture=_architecture(
                    providers=(provider.id,),
                    modes=("external_authoritative", "external_mirror"),
                ),
                external_providers=(provider,),
                capability_factories={
                    "connectors.readMirror": lambda _context: object()
                },
            )
        )

        with self.assertRaisesRegex(
            RegistryError,
            "require sanitized runtime state providers",
        ):
            registry.validate()

    def test_runtime_binding_states_are_validated_and_aggregated(self) -> None:
        observed_at = datetime(2026, 8, 1, 12, 0, tzinfo=UTC)

        def states(_context: ExternalProviderStateContext):
            return (
                ExternalProviderRuntimeState(
                    provider_id="calendar.caldav_sync",
                    binding_ref="calendar:sync-source:one",
                    authority_mode="governed_sync",
                    observed_at=observed_at,
                    configured=True,
                    active=True,
                    health="healthy",
                    freshness="current",
                    conflict="clear",
                    recovery="ready",
                ),
                ExternalProviderRuntimeState(
                    provider_id="calendar.caldav_sync",
                    binding_ref="calendar:sync-source:two",
                    authority_mode="external_mirror",
                    observed_at=observed_at,
                    configured=True,
                    active=True,
                    health="warning",
                    freshness="stale",
                    conflict="pending",
                    recovery="attention",
                ),
            )

        payload = collect_external_provider_states(
            (
                ExternalProviderStateProviderRegistration(
                    module_id="calendar",
                    provider_id="calendar.caldav_sync",
                    provider=states,
                ),
            ),
            ExternalProviderStateContext(session=None, tenant_id="tenant-1"),
        )["calendar.caldav_sync"]

        self.assertEqual("warning", payload["health"])
        self.assertEqual("stale", payload["freshness"])
        self.assertEqual("pending", payload["conflict"])
        self.assertEqual("attention", payload["recovery"])
        self.assertEqual(2, len(payload["bindings"]))

    def test_runtime_state_registration_requires_declared_provider(self) -> None:
        registry = PlatformRegistry()
        registry.register(
            ModuleManifest(
                id="calendar",
                name="Calendar",
                version="1.0.0",
                architecture=_architecture(),
                external_provider_state_providers=(
                    ExternalProviderStateProviderRegistration(
                        module_id="calendar",
                        provider_id="calendar.caldav_sync",
                        provider=lambda _context: (),
                    ),
                ),
            )
        )

        with self.assertRaisesRegex(RegistryError, "undeclared provider"):
            registry.validate()

    def test_runtime_state_failure_is_sanitized(self) -> None:
        def fail(_context: ExternalProviderStateContext):
            raise RuntimeError("secret transport detail")

        payload = collect_external_provider_states(
            (
                ExternalProviderStateProviderRegistration(
                    module_id="calendar",
                    provider_id="calendar.caldav_sync",
                    provider=fail,
                ),
            ),
            ExternalProviderStateContext(session=None),
        )["calendar.caldav_sync"]

        self.assertEqual("error", payload["health"])
        self.assertNotIn("secret transport detail", str(payload))

    def test_supported_claim_requires_evidence_not_only_a_string(self) -> None:
        registry = PlatformRegistry()
        registry.register(
            ModuleManifest(
                id="example",
                name="Example",
                version="1.0.0",
                architecture=ModuleArchitectureDeclaration(
                    layer="domain_capability",
                    kind="domain",
                    maturity="supported",
                    evidence=(),
                    owned_concepts=("example",),
                ),
            )
        )

        with self.assertRaisesRegex(RegistryError, "missing evidence"):
            registry.validate()

    def test_supported_and_lts_claims_require_reference_packages(self) -> None:
        for maturity in ("supported", "lts"):
            with self.subTest(maturity=maturity):
                declaration = ModuleArchitectureDeclaration(
                    layer="domain_capability",
                    kind="domain",
                    maturity=maturity,
                    evidence=tuple(
                        ModuleMaturityEvidence(
                            kind=kind,
                            reference=f"evidence/{kind}.json",
                            summary=f"{kind} evidence.",
                        )
                        for kind in (
                            "test",
                            "documentation",
                            "reference_process",
                            "target_test",
                            "recovery",
                            "security",
                            "operations",
                            "accessibility",
                            "privacy",
                            "upgrade",
                            *(("compatibility",) if maturity == "lts" else ()),
                        )
                    ),
                    owned_concepts=("example",),
                    documentation=ModuleArchitectureDocumentation(
                        upgrade=("docs/UPGRADE.md",),
                        recovery=("docs/RECOVERY.md",),
                        security=("docs/SECURITY.md",),
                        operations=("docs/OPERATIONS.md",),
                    ),
                )

                self.assertEqual(
                    (f"{maturity} maturity requires a reference package",),
                    module_architecture_issues(declaration, has_migrations=False),
                )

    def test_reference_ready_claim_requires_target_human_and_operator_evidence(self) -> None:
        declaration = ModuleArchitectureDeclaration(
            layer="domain_capability",
            kind="domain",
            maturity="reference_ready",
            evidence=(
                ModuleMaturityEvidence(
                    kind="test",
                    reference="tests/test_reference.py",
                    summary="Automated journey proof.",
                ),
                ModuleMaturityEvidence(
                    kind="documentation",
                    reference="docs/REFERENCE.md",
                    summary="Reference composition documentation.",
                ),
                ModuleMaturityEvidence(
                    kind="reference_process",
                    reference="docs/JOURNEY.md",
                    summary="Reference process definition.",
                ),
                ModuleMaturityEvidence(
                    kind="recovery",
                    reference="evidence/recovery.json",
                    summary="Recovery drill evidence.",
                ),
            ),
            known_limits=("One provider profile remains bounded.",),
            owned_concepts=("example",),
            reference_packages=("reference.example",),
        )

        issues = module_architecture_issues(declaration, has_migrations=False)

        self.assertIn("accessibility", issues[0])
        self.assertIn("operations", issues[0])
        self.assertIn("privacy", issues[0])
        self.assertIn("security", issues[0])
        self.assertIn("target_test", issues[0])
        self.assertIn("missing documentation", issues[1])

    def test_target_tested_provider_requires_provider_evidence(self) -> None:
        evidence = tuple(
            ModuleMaturityEvidence(
                kind=kind,
                reference=f"evidence/{kind}.json",
                summary=f"{kind} evidence.",
            )
            for kind in (
                "test",
                "documentation",
                "reference_process",
                "target_test",
                "recovery",
                "security",
                "operations",
                "accessibility",
                "privacy",
            )
        )
        declaration = ModuleArchitectureDeclaration(
            layer="data_reporting_integration",
            kind="integration",
            maturity="reference_ready",
            evidence=evidence,
            known_limits=("One bounded target profile is assessed.",),
            reference_packages=("reference.integration",),
            target_tested_providers=("connectors.example",),
            documentation=ModuleArchitectureDocumentation(
                recovery=("docs/RECOVERY.md",),
                security=("docs/SECURITY.md",),
                operations=("docs/OPERATIONS.md",),
            ),
        )

        self.assertEqual(
            (
                "maturity 'reference_ready' is missing evidence: provider",
            ),
            module_architecture_issues(declaration, has_migrations=False),
        )

    def test_provider_references_must_exist_in_manifest(self) -> None:
        provider = _mirror_provider()
        registry = PlatformRegistry()
        registry.register(
            ModuleManifest(
                id="connectors",
                name="Connectors",
                version="1.0.0",
                architecture=_architecture(
                    providers=(provider.id,),
                    modes=("external_authoritative", "external_mirror"),
                ),
                external_providers=(provider,),
            )
        )

        with self.assertRaisesRegex(RegistryError, "undeclared capabilities"):
            registry.validate()

    def test_governed_sync_requires_read_and_write_semantics(self) -> None:
        with self.assertRaisesRegex(
            ArchitectureDeclarationError,
            "read and write/synchronize",
        ):
            ExternalProviderDeclaration(
                id="broken.sync",
                module_id="broken",
                label="Broken sync",
                maturity="synchronize",
                operations=("synchronize",),
                objects=(
                    ProviderObjectDeclaration(
                        object_type="object",
                        field_groups=("content",),
                        authority_modes=("governed_sync",),
                        default_authority_mode="governed_sync",
                    ),
                ),
                behavior=_writable_provider().behavior,
            )


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