from __future__ import annotations

import unittest

from govoplan_core.core.automation import (
    ActionDefinition,
    ActionEffectProvider,
    ActionExecutionRequest,
    ActionExecutionResult,
    ActionPreview,
    CAPABILITY_AUTH_AUTOMATION_PRINCIPAL_PROVIDER,
    AutomationInvocation,
    AutomationPrincipalProvider,
    AutomationPrincipalRequest,
    AutomationPrincipalResolution,
    EffectDefinition,
    EffectPreview,
    ObservedEffect,
    action_effect_provider,
    automation_principal_provider,
)
from govoplan_core.core.modules import ModuleContext, ModuleManifest
from govoplan_core.core.registry import PlatformRegistry


class _Provider:
    def resolve_automation_principal(self, session, *, request):
        del session
        return AutomationPrincipalResolution(
            allowed=True,
            principal={"account_id": request.account_id},
            granted_scopes=request.grant_scopes,
        )


class _ActionProvider:
    action = ActionDefinition(
        action_key="postbox.message.deliver",
        owner_module="postbox",
        description="Deliver one governed Postbox message.",
        input_schema_ref="schema:postbox.message.deliver@1",
        required_scopes=("postbox:message:write",),
        required_capabilities=("postbox.delivery",),
        risk_level="high",
        reversibility="compensatable",
        expected_effect_keys=("postbox.message.created",),
        audit_event_types=("postbox.message.delivered",),
    )
    effect = EffectDefinition(
        effect_key="postbox.message.created",
        owner_module="postbox",
        operation="created",
        description="A durable Postbox message was created.",
        resource_types=("postbox_message",),
    )

    def action_definitions(self):
        return (self.action,)

    def effect_definitions(self):
        return (self.effect,)

    def preview_action(self, session, principal, *, request):
        del session, principal
        return ActionPreview(
            action_key=request.action_key,
            allowed=True,
            summary="One Postbox message will be created.",
            risk_level=self.action.risk_level,
            reversibility=self.action.reversibility,
            effects=(
                EffectPreview(
                    effect_key=self.effect.effect_key,
                    summary="Create message.",
                ),
            ),
            preview_ref="preview:1",
        )

    def execute_action(self, session, principal, *, request):
        del session, principal
        return ActionExecutionResult(
            state="completed",
            output={"message_ref": "postbox-message:1"},
            observed_effects=(
                ObservedEffect(
                    effect_key=self.effect.effect_key,
                    operation="created",
                    resource_ref="postbox-message:1",
                ),
            ),
            audit_event_refs=("audit-event:1",),
        )


class AutomationContractTests(unittest.TestCase):
    def test_principal_request_has_explicit_subject_contracts(self) -> None:
        service = AutomationPrincipalRequest.service_account(
            tenant_id="tenant-1",
            service_account_id="service-1",
            authorization_ref="trigger:1",
            grant_scopes=("dataflow:pipeline:run",),
        )

        self.assertEqual("service_account", service.subject_kind)
        self.assertEqual("service-1", service.service_account_id)
        self.assertIsNone(service.account_id)
        with self.assertRaisesRegex(
            ValueError,
            "Delegated-user automation",
        ):
            AutomationPrincipalRequest(
                tenant_id="tenant-1",
                authorization_ref="trigger:1",
                grant_scopes=("dataflow:pipeline:run",),
            )

    def test_invocation_records_stable_trigger_provenance(self) -> None:
        invocation = AutomationInvocation(
            kind="event",
            trigger_ref="dataflow-trigger:1",
            event_id="event-1",
            event_type="files.uploaded",
        )

        self.assertEqual("event", invocation.kind)
        self.assertEqual("event-1", invocation.event_id)

    def test_principal_provider_is_runtime_resolved(self) -> None:
        provider = _Provider()
        self.assertIsInstance(provider, AutomationPrincipalProvider)
        registry = PlatformRegistry()
        registry.register(
            ModuleManifest(
                id="automation_contract_test",
                name="Automation contract test",
                version="test",
                capability_factories={
                    CAPABILITY_AUTH_AUTOMATION_PRINCIPAL_PROVIDER: (
                        lambda context: provider
                    ),
                },
            )
        )
        registry.configure_capability_context(
            ModuleContext(registry=registry, settings=object())
        )

        self.assertIs(provider, automation_principal_provider(registry))
        result = provider.resolve_automation_principal(
            object(),
            request=AutomationPrincipalRequest(
                tenant_id="tenant-1",
                account_id="account-1",
                membership_id="membership-1",
                authorization_ref="trigger:1",
                grant_scopes=("dataflow:pipeline:run",),
            ),
        )
        self.assertTrue(result.allowed)
        self.assertEqual(("dataflow:pipeline:run",), result.granted_scopes)

    def test_action_effect_provider_is_previewable_and_idempotent_by_contract(
        self,
    ) -> None:
        provider = _ActionProvider()
        self.assertIsInstance(provider, ActionEffectProvider)
        request = ActionExecutionRequest(
            tenant_id="tenant-1",
            action_key=provider.action.action_key,
            input={"postbox_ref": "postbox:1"},
            idempotency_key="workflow:instance-1:step-2:attempt-1",
            invocation=AutomationInvocation(
                kind="workflow",
                trigger_ref="workflow-instance:1",
            ),
        )

        preview = provider.preview_action(object(), object(), request=request)
        result = provider.execute_action(object(), object(), request=request)

        self.assertTrue(preview.allowed)
        self.assertEqual("compensatable", preview.reversibility)
        self.assertEqual("completed", result.state)
        self.assertEqual(
            "postbox-message:1",
            result.observed_effects[0].resource_ref,
        )

    def test_action_effect_provider_is_resolved_by_capability_name(self) -> None:
        provider = _ActionProvider()
        registry = PlatformRegistry()
        registry.register(
            ModuleManifest(
                id="action_contract_test",
                name="Action contract test",
                version="test",
                capability_factories={
                    "postbox.actions": lambda context: provider,
                    "invalid.actions": lambda context: object(),
                },
            )
        )
        registry.configure_capability_context(
            ModuleContext(registry=registry, settings=object())
        )

        self.assertIs(
            provider,
            action_effect_provider(registry, "postbox.actions"),
        )
        self.assertIsNone(
            action_effect_provider(registry, "invalid.actions")
        )
        self.assertIsNone(
            action_effect_provider(registry, "missing.actions")
        )

    def test_action_contract_rejects_unversioned_or_incomplete_definitions(
        self,
    ) -> None:
        with self.assertRaisesRegex(ValueError, "input schema"):
            ActionDefinition(
                action_key="invalid",
                owner_module="test",
                description="Invalid action",
                input_schema_ref="",
            )
        with self.assertRaisesRegex(ValueError, "contract version"):
            EffectDefinition(
                effect_key="test.effect",
                owner_module="test",
                operation="changed",
                description="Test effect",
                contract_version="2",
            )


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