from __future__ import annotations

from datetime import datetime, timedelta, timezone
import unittest

from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker

from govoplan_core.core.modules import ModuleManifest
from govoplan_core.core.ownership import (
    OwnershipActionDecision,
    OwnershipIdempotencyConflict,
    OwnershipProviderRegistration,
    OwnershipResourceRef,
    OwnershipSubjectRef,
    OwnershipTransfer,
    OwnershipTransferError,
    OwnershipTransferExpired,
    OwnershipTransferPolicy,
    OwnershipTransferStatus,
    accept_ownership_transfer,
    approve_administrative_recovery,
    approve_ownership_request,
    execute_administrative_recovery,
    request_ownership,
    start_administrative_recovery,
    start_owner_initiated_transfer,
)
from govoplan_core.core.registry import PlatformRegistry
from govoplan_core.core.runtime import (
    clear_runtime,
    configure_runtime,
    get_runtime_context,
)
from govoplan_core.db.base import Base


NOW = datetime(2026, 7, 30, 10, 0, tzinfo=timezone.utc)
RESOURCE = OwnershipResourceRef(
    module_id="campaigns",
    resource_type="campaign",
    resource_id="campaign-1",
)
OWNER = OwnershipSubjectRef(type="account", id="owner-1")
TARGET = OwnershipSubjectRef(type="account", id="target-1")


class _OwnershipProvider:
    def __init__(self) -> None:
        self.owner = OWNER
        self.applied: list[tuple[OwnershipSubjectRef, OwnershipSubjectRef]] = []

    def current_owner(self, session, *, tenant_id: str, resource_id: str):
        if tenant_id != "tenant-1" or resource_id != RESOURCE.resource_id:
            return None
        return self.owner

    def authorize_ownership_action(
        self,
        session,
        *,
        tenant_id: str,
        resource_id: str,
        action: str,
        actor: OwnershipSubjectRef,
        current_owner: OwnershipSubjectRef,
        target_owner: OwnershipSubjectRef,
    ) -> OwnershipActionDecision:
        allowed = False
        if action in {"propose_transfer", "approve_requested_transfer"}:
            allowed = actor.id == current_owner.id
        elif action == "request_ownership":
            allowed = actor.id == target_owner.id
        elif action == "accept_transfer":
            allowed = actor.id == target_owner.id
        elif action == "accept_group_transfer":
            allowed = actor.id == "group-manager-1"
        elif action in {
            "request_recovery",
            "approve_recovery",
            "execute_recovery",
        }:
            allowed = actor.id.startswith("recovery-officer-")
        elif action in {"decline_transfer", "cancel_transfer"}:
            allowed = actor.id in {current_owner.id, target_owner.id}
        return OwnershipActionDecision(
            allowed=allowed,
            reason=None if allowed else f"Not authorized for {action}",
        )

    def apply_owner(
        self,
        session,
        *,
        tenant_id: str,
        resource_id: str,
        expected_owner: OwnershipSubjectRef,
        target_owner: OwnershipSubjectRef,
        actor: OwnershipSubjectRef,
        reason: str | None,
    ) -> None:
        if self.owner != expected_owner:
            raise OwnershipTransferError("Owner changed while transfer was pending")
        self.applied.append((expected_owner, target_owner))
        self.owner = target_owner


class OwnershipTransferTests(unittest.TestCase):
    def setUp(self) -> None:
        self.runtime_context = get_runtime_context()
        clear_runtime()
        self.engine = create_engine("sqlite:///:memory:")
        Base.metadata.create_all(
            self.engine,
            tables=[OwnershipTransfer.__table__],
        )
        self.Session = sessionmaker(bind=self.engine, expire_on_commit=False)
        self.session: Session = self.Session()
        self.provider = _OwnershipProvider()

    def tearDown(self) -> None:
        self.session.close()
        Base.metadata.drop_all(
            self.engine,
            tables=[OwnershipTransfer.__table__],
        )
        self.engine.dispose()
        if self.runtime_context is not None:
            configure_runtime(self.runtime_context)

    def test_owner_initiated_transfer_requires_target_acceptance(self) -> None:
        transfer = start_owner_initiated_transfer(
            self.session,
            tenant_id="tenant-1",
            resource=RESOURCE,
            provider=self.provider,
            actor=OWNER,
            target_owner=TARGET,
            idempotency_key="owner-transfer-1",
            now=NOW,
        )

        self.assertEqual(
            transfer.status,
            OwnershipTransferStatus.AWAITING_TARGET_ACCEPTANCE.value,
        )
        self.assertEqual(self.provider.owner, OWNER)

        accept_ownership_transfer(
            self.session,
            transfer=transfer,
            provider=self.provider,
            actor=TARGET,
            now=NOW + timedelta(hours=1),
        )

        self.assertEqual(transfer.status, OwnershipTransferStatus.COMPLETED.value)
        self.assertEqual(self.provider.owner, TARGET)
        self.assertEqual(transfer.target_accepted_by_id, TARGET.id)
        self.assertEqual(
            ["started", "accepted"],
            [decision["action"] for decision in transfer.decisions],
        )

    def test_requested_transfer_requires_owner_then_requester(self) -> None:
        transfer = request_ownership(
            self.session,
            tenant_id="tenant-1",
            resource=RESOURCE,
            provider=self.provider,
            actor=TARGET,
            idempotency_key="request-transfer-1",
            now=NOW,
        )
        self.assertEqual(
            transfer.status,
            OwnershipTransferStatus.AWAITING_OWNER_APPROVAL.value,
        )

        approve_ownership_request(
            self.session,
            transfer=transfer,
            provider=self.provider,
            actor=OWNER,
            now=NOW + timedelta(minutes=5),
        )
        self.assertEqual(
            transfer.status,
            OwnershipTransferStatus.AWAITING_TARGET_ACCEPTANCE.value,
        )
        self.assertEqual(self.provider.owner, OWNER)

        accept_ownership_transfer(
            self.session,
            transfer=transfer,
            provider=self.provider,
            actor=TARGET,
            now=NOW + timedelta(minutes=10),
        )
        self.assertEqual(self.provider.owner, TARGET)

    def test_idempotency_key_replays_only_the_same_request(self) -> None:
        first = start_owner_initiated_transfer(
            self.session,
            tenant_id="tenant-1",
            resource=RESOURCE,
            provider=self.provider,
            actor=OWNER,
            target_owner=TARGET,
            idempotency_key="same-key",
            now=NOW,
        )
        replay = start_owner_initiated_transfer(
            self.session,
            tenant_id="tenant-1",
            resource=RESOURCE,
            provider=self.provider,
            actor=OWNER,
            target_owner=TARGET,
            idempotency_key="same-key",
            now=NOW,
        )
        self.assertEqual(replay.id, first.id)

        with self.assertRaises(OwnershipIdempotencyConflict):
            start_owner_initiated_transfer(
                self.session,
                tenant_id="tenant-1",
                resource=RESOURCE,
                provider=self.provider,
                actor=OWNER,
                target_owner=OwnershipSubjectRef(type="account", id="other"),
                idempotency_key="same-key",
                now=NOW,
            )

    def test_group_target_acceptance_uses_dedicated_authority(self) -> None:
        group = OwnershipSubjectRef(type="group", id="group-1")
        transfer = start_owner_initiated_transfer(
            self.session,
            tenant_id="tenant-1",
            resource=RESOURCE,
            provider=self.provider,
            actor=OWNER,
            target_owner=group,
            idempotency_key="group-transfer",
            now=NOW,
        )

        accept_ownership_transfer(
            self.session,
            transfer=transfer,
            provider=self.provider,
            actor=OwnershipSubjectRef(type="account", id="group-manager-1"),
            now=NOW + timedelta(minutes=10),
        )

        self.assertEqual(self.provider.owner, group)
        self.assertEqual(transfer.target_accepted_by_id, "group-manager-1")

    def test_standard_recovery_requires_quorum_and_delay(self) -> None:
        policy = OwnershipTransferPolicy()
        target = OwnershipSubjectRef(type="account", id="recovered-owner")
        transfer = start_administrative_recovery(
            self.session,
            tenant_id="tenant-1",
            resource=RESOURCE,
            provider=self.provider,
            actor=OwnershipSubjectRef(
                type="account",
                id="recovery-officer-1",
                recently_authenticated=True,
            ),
            target_owner=target,
            idempotency_key="recovery-1",
            reason="Owner is unavailable",
            policy=policy,
            now=NOW,
        )
        self.assertFalse(transfer.metadata_["encryption_keys_included"])

        approve_administrative_recovery(
            self.session,
            transfer=transfer,
            provider=self.provider,
            actor=OwnershipSubjectRef(
                type="account",
                id="recovery-officer-1",
                recently_authenticated=True,
            ),
            now=NOW,
        )
        self.assertEqual(
            transfer.status,
            OwnershipTransferStatus.AWAITING_RECOVERY_APPROVALS.value,
        )
        approve_administrative_recovery(
            self.session,
            transfer=transfer,
            provider=self.provider,
            actor=OwnershipSubjectRef(
                type="account",
                id="recovery-officer-2",
                recently_authenticated=True,
            ),
            now=NOW + timedelta(minutes=1),
        )
        self.assertEqual(
            transfer.status,
            OwnershipTransferStatus.RECOVERY_SCHEDULED.value,
        )

        with self.assertRaisesRegex(
            OwnershipTransferError,
            "delay has not elapsed",
        ):
            execute_administrative_recovery(
                self.session,
                transfer=transfer,
                provider=self.provider,
                actor=OwnershipSubjectRef(
                    type="account",
                    id="recovery-officer-3",
                    recently_authenticated=True,
                ),
                now=NOW + timedelta(hours=23),
            )

        execute_administrative_recovery(
            self.session,
            transfer=transfer,
            provider=self.provider,
            actor=OwnershipSubjectRef(
                type="account",
                id="recovery-officer-3",
                recently_authenticated=True,
            ),
            now=NOW + timedelta(hours=24),
        )
        self.assertEqual(self.provider.owner, target)
        self.assertEqual(
            [
                "started",
                "recovery_approved",
                "recovery_approved",
                "recovery_executed",
            ],
            [decision["action"] for decision in transfer.decisions],
        )
        self.assertEqual(
            2,
            transfer.decisions[0]["details"]["required_approvals"],
        )

    def test_standard_recovery_rejects_stale_authentication_context(self) -> None:
        with self.assertRaisesRegex(
            PermissionError,
            "recent authentication",
        ):
            start_administrative_recovery(
                self.session,
                tenant_id="tenant-1",
                resource=RESOURCE,
                provider=self.provider,
                actor=OwnershipSubjectRef(
                    type="account",
                    id="recovery-officer-1",
                ),
                target_owner=OwnershipSubjectRef(
                    type="account",
                    id="recovered-owner",
                ),
                idempotency_key="stale-recovery",
                reason="Owner is unavailable",
                now=NOW,
            )

    def test_expiry_is_recorded_before_a_late_decision_is_rejected(self) -> None:
        transfer = start_owner_initiated_transfer(
            self.session,
            tenant_id="tenant-1",
            resource=RESOURCE,
            provider=self.provider,
            actor=OWNER,
            target_owner=TARGET,
            idempotency_key="expiring-transfer",
            expiry_days=1,
            now=NOW,
        )

        with self.assertRaises(OwnershipTransferExpired):
            accept_ownership_transfer(
                self.session,
                transfer=transfer,
                provider=self.provider,
                actor=TARGET,
                now=NOW + timedelta(days=2),
            )
        self.session.commit()

        self.assertEqual(
            OwnershipTransferStatus.EXPIRED.value,
            transfer.status,
        )
        self.assertEqual(
            ["started", "expired"],
            [decision["action"] for decision in transfer.decisions],
        )

    def test_registry_resolves_one_provider_per_resource_type(self) -> None:
        registry = PlatformRegistry()
        registry.register(
            ModuleManifest(
                id="campaigns",
                name="Campaigns",
                version="1.0.0",
                ownership_providers=(
                    OwnershipProviderRegistration(
                        resource_type="campaign",
                        provider=self.provider,
                    ),
                ),
            )
        )

        self.assertIs(registry.ownership_provider("CAMPAIGN"), self.provider)


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