"""Runtime requirement contract: capabilities declare hosting-neutral routes/config/
platform-service needs, and ``merge_runtime_requirements`` combines them
deterministically before any hosting adapter sees them.

Uses fake provisioners (not Auth/Forms directly) so these tests validate the
abstraction itself, independent of real Auth/Forms implementation details -- the real
``AuthV2Provisioner``/``FormsProvisioner``/``FeaturePackProvisioner`` adapters are
covered by ``test_capability_lifecycle_adapters.py``.
"""
from __future__ import annotations

from uuid import uuid4

import pytest
from src.domain.capabilities.lifecycle import (
    CapabilityProvisioner,
    CapabilityState,
    CapabilityStatus,
    ProvisionPlan,
    ProvisionResult,
)
from src.domain.capabilities.runtime import (
    CapabilityRuntimeRequirements,
    RuntimeConfigRequirement,
    RuntimeRouteRequirement,
    merge_runtime_requirements,
)


class FakeAuthCapability(CapabilityProvisioner):
    capability = "auth"
    version = 2

    async def declare(self, project, desired_state):
        return {}

    async def plan(self, project) -> ProvisionPlan:
        return ProvisionPlan(self.capability, self.version)

    async def apply(self, project, plan) -> ProvisionResult:
        return ProvisionResult(self.capability, self.version, CapabilityState.READY)

    async def reconcile(self, project) -> ProvisionResult:
        return await self.apply(project, await self.plan(project))

    async def status(self, project) -> CapabilityStatus:
        return CapabilityStatus(
            capability=self.capability, version=self.version, state=CapabilityState.READY
        )

    async def runtime_requirements(self, project) -> CapabilityRuntimeRequirements:
        return CapabilityRuntimeRequirements(
            capability_key=self.capability,
            capability_version=self.version,
            routes=(
                RuntimeRouteRequirement(
                    path_prefix="/api/auth", purpose="auth_session_and_signin_routes"
                ),
            ),
            platform_services=("p2hs-auth",),
            config=(
                RuntimeConfigRequirement(key="P2HS_AUTH_PUBLIC_KEY", visibility="public"),
                RuntimeConfigRequirement(key="P2HS_CLOUD_SERVICE_TOKEN", visibility="trusted"),
            ),
        )


class FakeFormsCapability(CapabilityProvisioner):
    capability = "forms"
    version = 1

    async def declare(self, project, desired_state):
        return {}

    async def plan(self, project) -> ProvisionPlan:
        return ProvisionPlan(self.capability, self.version)

    async def apply(self, project, plan) -> ProvisionResult:
        return ProvisionResult(self.capability, self.version, CapabilityState.READY)

    async def reconcile(self, project) -> ProvisionResult:
        return await self.apply(project, await self.plan(project))

    async def status(self, project) -> CapabilityStatus:
        return CapabilityStatus(
            capability=self.capability, version=self.version, state=CapabilityState.READY
        )

    async def runtime_requirements(self, project) -> CapabilityRuntimeRequirements:
        return CapabilityRuntimeRequirements(
            capability_key=self.capability,
            capability_version=self.version,
            routes=(
                RuntimeRouteRequirement(
                    path_prefix="/api/forms", purpose="form_submission_routes"
                ),
            ),
            platform_services=("p2hs-forms",),
        )


class FakeConflictingCapability(CapabilityProvisioner):
    """Deliberately claims Auth's route with a different purpose and Auth's public
    config key as trusted, to exercise conflict detection."""

    capability = "conflicting"
    version = 1

    async def declare(self, project, desired_state):
        return {}

    async def plan(self, project) -> ProvisionPlan:
        return ProvisionPlan(self.capability, self.version)

    async def apply(self, project, plan) -> ProvisionResult:
        return ProvisionResult(self.capability, self.version, CapabilityState.READY)

    async def reconcile(self, project) -> ProvisionResult:
        return await self.apply(project, await self.plan(project))

    async def status(self, project) -> CapabilityStatus:
        return CapabilityStatus(
            capability=self.capability, version=self.version, state=CapabilityState.READY
        )

    async def runtime_requirements(self, project) -> CapabilityRuntimeRequirements:
        return CapabilityRuntimeRequirements(
            capability_key=self.capability,
            capability_version=self.version,
            routes=(
                RuntimeRouteRequirement(path_prefix="/api/auth", purpose="unrelated_purpose"),
            ),
            config=(
                RuntimeConfigRequirement(key="P2HS_AUTH_PUBLIC_KEY", visibility="trusted"),
            ),
        )


@pytest.mark.asyncio
async def test_fake_capabilities_expose_runtime_requirements():
    project_id = uuid4()
    auth_req = await FakeAuthCapability().runtime_requirements(project_id)
    forms_req = await FakeFormsCapability().runtime_requirements(project_id)

    assert auth_req.capability_key == "auth"
    assert forms_req.capability_key == "forms"
    assert auth_req.routes[0].path_prefix == "/api/auth"
    assert forms_req.routes[0].path_prefix == "/api/forms"


@pytest.mark.asyncio
async def test_merge_is_deterministic_across_repeated_calls():
    project_id = uuid4()
    requirements = [
        await FakeAuthCapability().runtime_requirements(project_id),
        await FakeFormsCapability().runtime_requirements(project_id),
    ]

    first = merge_runtime_requirements(requirements)
    second = merge_runtime_requirements(requirements)

    assert first.ok and second.ok
    first_prefixes = [r.path_prefix for r in first.merged.routes]
    second_prefixes = [r.path_prefix for r in second.merged.routes]
    assert first_prefixes == second_prefixes
    assert first.merged.platform_services == second.merged.platform_services
    assert first.merged.platform_services == ("p2hs-auth", "p2hs-forms")


def test_duplicate_routes_from_repeated_declaration_deduplicate():
    same_route = RuntimeRouteRequirement(
        path_prefix="/api/forms", purpose="form_submission_routes"
    )
    first = CapabilityRuntimeRequirements(
        capability_key="forms", capability_version=1, routes=(same_route,)
    )
    second = CapabilityRuntimeRequirements(
        capability_key="forms", capability_version=1, routes=(same_route,)
    )

    result = merge_runtime_requirements([first, second])

    assert result.ok
    assert len(result.merged.routes) == 1


def test_conflicting_route_purpose_fails_closed_without_silently_overwriting():
    auth = CapabilityRuntimeRequirements(
        capability_key="auth",
        capability_version=2,
        routes=(
            RuntimeRouteRequirement(
                path_prefix="/api/auth", purpose="auth_session_and_signin_routes"
            ),
        ),
    )
    conflicting = CapabilityRuntimeRequirements(
        capability_key="conflicting",
        capability_version=1,
        routes=(RuntimeRouteRequirement(path_prefix="/api/auth", purpose="unrelated_purpose"),),
    )

    result = merge_runtime_requirements([auth, conflicting])

    assert result.ok is False
    assert result.merged is None
    assert any(c.kind == "route_purpose_conflict" for c in result.conflicts)


def test_conflicting_trusted_vs_public_config_fails_closed():
    auth = CapabilityRuntimeRequirements(
        capability_key="auth",
        capability_version=2,
        config=(RuntimeConfigRequirement(key="P2HS_AUTH_PUBLIC_KEY", visibility="public"),),
    )
    conflicting = CapabilityRuntimeRequirements(
        capability_key="conflicting",
        capability_version=1,
        config=(RuntimeConfigRequirement(key="P2HS_AUTH_PUBLIC_KEY", visibility="trusted"),),
    )

    result = merge_runtime_requirements([auth, conflicting])

    assert result.ok is False
    assert any(c.kind == "config_visibility_conflict" for c in result.conflicts)


def test_required_true_wins_over_required_false_for_same_config_key():
    optional = CapabilityRuntimeRequirements(
        capability_key="auth",
        capability_version=2,
        config=(RuntimeConfigRequirement(key="SOME_KEY", visibility="trusted", required=False),),
    )
    required = CapabilityRuntimeRequirements(
        capability_key="forms",
        capability_version=1,
        config=(RuntimeConfigRequirement(key="SOME_KEY", visibility="trusted", required=True),),
    )

    result = merge_runtime_requirements([optional, required])

    assert result.ok
    assert result.merged.config[0].required is True


def test_empty_requirements_merge_to_an_empty_ok_result():
    result = merge_runtime_requirements([])

    assert result.ok
    assert result.merged.routes == ()
    assert result.merged.platform_services == ()


def test_runtime_requirements_round_trip_through_dict():
    original = CapabilityRuntimeRequirements(
        capability_key="auth",
        capability_version=2,
        routes=(
            RuntimeRouteRequirement(
                path_prefix="/api/auth",
                purpose="auth_session_and_signin_routes",
                methods=("GET", "POST"),
            ),
        ),
        platform_services=("p2hs-auth",),
        config=(
            RuntimeConfigRequirement(
                key="P2HS_AUTH_PUBLIC_KEY", visibility="public", description="public half"
            ),
        ),
    )

    restored = CapabilityRuntimeRequirements.from_dict(original.to_dict())

    assert restored == original


@pytest.mark.asyncio
async def test_hosting_projection_only_needs_the_merged_result_not_capability_objects():
    """No capability needs to know which hosting provider is selected: a caller
    building a hosting plan only ever touches the merged, hosting-neutral result."""
    project_id = uuid4()
    requirements = [
        await FakeAuthCapability().runtime_requirements(project_id),
        await FakeFormsCapability().runtime_requirements(project_id),
    ]

    merged = merge_runtime_requirements(requirements).merged

    # A stand-in for a RuntimeProjectionAdapter: it only ever receives the merged,
    # hosting-neutral object -- never a FakeAuthCapability/FakeFormsCapability, and
    # never anything naming a hosting provider.
    def fake_project_routes(merged_requirements):
        return {route.path_prefix: route.purpose for route in merged_requirements.routes}

    projected = fake_project_routes(merged)
    assert projected == {
        "/api/auth": "auth_session_and_signin_routes",
        "/api/forms": "form_submission_routes",
    }
