import pytest
from collections.abc import Mapping

from app.agents.prompts import (
    SUB_COMMANDER_PROMPTS_BY_KEY,
    TOP_LEVEL_SYSTEM_PROMPTS_BY_KEY,
)
from app.agents.registry import build_registry_indexes, load_builtin_registry_bundle, load_builtin_registry_indexes
from app.agents.registry.indexes import summarize_registry_indexes
from app.agents.registry.models import (
    AgentManifest,
    CapabilityManifest,
    PermissionClass,
    SideEffectScope,
    SpecialistTemplateManifest,
    SubCommanderManifest,
)
from app.agents.registry.validator import validate_registry_bundle
from app.agents.registry.builtins import (
    BUILTIN_AGENT_MANIFESTS,
    BUILTIN_CAPABILITY_MANIFESTS,
    BUILTIN_SPECIALIST_TEMPLATE_MANIFESTS,
    BUILTIN_SUB_COMMANDER_MANIFESTS,
)
from app.agents.state import AgentRole
from app.agents.tools import SUB_COMMANDER_TOOLSETS


def make_agent(
    agent_id: str = "master",
    *,
    display_name: str = "Master",
    role_value: str = "master",
    system_prompt_key: str = "master",
    default_sub_commanders: list[str] | None = None,
) -> AgentManifest:
    return AgentManifest(
        agent_id=agent_id,
        display_name=display_name,
        role_value=role_value,
        system_prompt_key=system_prompt_key,
        routing_hints=["route"],
        default_sub_commanders=default_sub_commanders or [],
    )


def make_sub_commander(
    sub_commander_id: str = "planner",
    *,
    parent_agent_id: str = "master",
    capability_ids: list[str] | None = None,
) -> SubCommanderManifest:
    return SubCommanderManifest(
        sub_commander_id=sub_commander_id,
        parent_agent_id=parent_agent_id,
        prompt_text="Plan the work.",
        capability_ids=capability_ids or [],
    )


def make_capability(capability_id: str = "calendar") -> CapabilityManifest:
    return CapabilityManifest(capability_id=capability_id, tool_name=f"{capability_id}_tool")


def make_specialist_template(
    template_id: str = "researcher",
    *,
    allowed_capability_ids: list[str] | None = None,
) -> SpecialistTemplateManifest:
    return SpecialistTemplateManifest(
        template_id=template_id,
        display_name="Researcher",
        description="Research specialist",
        allowed_capability_ids=allowed_capability_ids,
    )


def test_validate_registry_bundle_accepts_valid_bundle() -> None:
    validate_registry_bundle(
        agents=[make_agent(default_sub_commanders=["planner"])],
        sub_commanders=[make_sub_commander(capability_ids=["calendar"])],
        capabilities=[make_capability()],
        specialist_templates=[make_specialist_template(allowed_capability_ids=["calendar"])],
    )


def test_validate_registry_bundle_rejects_duplicate_agent_ids() -> None:
    agents = [
        make_agent(default_sub_commanders=["planner"]),
        make_agent(
            display_name="Duplicate Master",
            role_value="master_duplicate",
            system_prompt_key="master_duplicate",
        ),
    ]

    with pytest.raises(ValueError, match="duplicate agent id: master"):
        validate_registry_bundle(
            agents=agents,
            sub_commanders=[],
            capabilities=[],
            specialist_templates=[],
        )


def test_validate_registry_bundle_rejects_duplicate_sub_commander_ids() -> None:
    with pytest.raises(ValueError, match="duplicate sub commander id: planner"):
        validate_registry_bundle(
            agents=[make_agent()],
            sub_commanders=[make_sub_commander(), make_sub_commander()],
            capabilities=[],
            specialist_templates=[],
        )


def test_validate_registry_bundle_rejects_duplicate_capability_ids() -> None:
    with pytest.raises(ValueError, match="duplicate capability id: calendar"):
        validate_registry_bundle(
            agents=[],
            sub_commanders=[],
            capabilities=[make_capability(), make_capability()],
            specialist_templates=[],
        )


def test_validate_registry_bundle_rejects_duplicate_template_ids() -> None:
    with pytest.raises(ValueError, match="duplicate template id: researcher"):
        validate_registry_bundle(
            agents=[],
            sub_commanders=[],
            capabilities=[],
            specialist_templates=[make_specialist_template(), make_specialist_template()],
        )


def test_validate_registry_bundle_rejects_unknown_sub_commander_parent_agent_ids() -> None:
    sub_commanders = [make_sub_commander(parent_agent_id="missing-agent")]

    with pytest.raises(ValueError, match="unknown parent agent id: missing-agent"):
        validate_registry_bundle(
            agents=[],
            sub_commanders=sub_commanders,
            capabilities=[],
            specialist_templates=[],
        )


def test_validate_registry_bundle_rejects_unknown_sub_commander_capability_references() -> None:
    with pytest.raises(ValueError, match="unknown capability id: search"):
        validate_registry_bundle(
            agents=[make_agent(default_sub_commanders=["planner"])],
            sub_commanders=[make_sub_commander(capability_ids=["search"])],
            capabilities=[make_capability()],
            specialist_templates=[],
        )


def test_validate_registry_bundle_rejects_unknown_specialist_template_capability_references() -> None:
    with pytest.raises(ValueError, match="unknown capability id: missing-capability"):
        validate_registry_bundle(
            agents=[],
            sub_commanders=[],
            capabilities=[make_capability()],
            specialist_templates=[
                make_specialist_template(allowed_capability_ids=["missing-capability"])
            ],
        )


def test_registry_bundle_agent_roles_match_runtime_agent_role_enum_values() -> None:
    bundle = load_builtin_registry_bundle()
    indexes = build_registry_indexes(bundle)

    assert set(indexes.agent_by_id) == {role.value for role in AgentRole}
    assert {agent.role_value for agent in bundle.agents} == {role.value for role in AgentRole}


def test_registry_bundle_agent_system_prompt_keys_match_runtime_top_level_prompt_surface() -> None:
    bundle = load_builtin_registry_bundle()
    indexes = build_registry_indexes(bundle)

    expected_prompt_keys_by_agent_id = {
        role.value: role.value for role in AgentRole if role.value in TOP_LEVEL_SYSTEM_PROMPTS_BY_KEY
    }

    assert set(TOP_LEVEL_SYSTEM_PROMPTS_BY_KEY) == {role.value for role in AgentRole}
    assert indexes.agent_prompt_key_by_id == expected_prompt_keys_by_agent_id
    assert {
        agent.agent_id: TOP_LEVEL_SYSTEM_PROMPTS_BY_KEY[agent.system_prompt_key]
        for agent in bundle.agents
    } == {
        role.value: TOP_LEVEL_SYSTEM_PROMPTS_BY_KEY[role.value]
        for role in AgentRole
    }


def test_registry_bundle_skill_context_keys_match_graph_role_derivation_rule() -> None:
    bundle = load_builtin_registry_bundle()
    indexes = build_registry_indexes(bundle)

    expected_skill_context_keys = {
        role.value: role.value.replace("agent_", "")
        for role in AgentRole
    }

    assert indexes.skill_context_key_by_agent_id == expected_skill_context_keys
    assert {
        agent.agent_id: agent.skill_context_key for agent in bundle.agents
    } == expected_skill_context_keys


def test_registry_bundle_sub_commander_prompt_texts_match_runtime_prompt_map() -> None:
    bundle = load_builtin_registry_bundle()
    indexes = build_registry_indexes(bundle)

    assert set(indexes.sub_commander_by_id) == set(SUB_COMMANDER_PROMPTS_BY_KEY)
    assert indexes.sub_commander_prompt_key_by_id == {
        sub_commander_id: sub_commander_id
        for sub_commander_id in SUB_COMMANDER_PROMPTS_BY_KEY
    }
    assert {
        sub_commander.sub_commander_id: sub_commander.prompt_text
        for sub_commander in bundle.sub_commanders
    } == SUB_COMMANDER_PROMPTS_BY_KEY


def test_registry_bundle_sub_commander_tool_membership_and_order_match_runtime_toolsets() -> None:
    bundle = load_builtin_registry_bundle()
    indexes = build_registry_indexes(bundle)

    assert set(indexes.sub_commander_by_id) == set(SUB_COMMANDER_TOOLSETS)
    assert indexes.capability_ids_by_sub_commander_id == {
        sub_commander_id: tuple(tool.name for tool in tools)
        for sub_commander_id, tools in SUB_COMMANDER_TOOLSETS.items()
    }
    assert {
        sub_commander.sub_commander_id: tuple(sub_commander.capability_ids)
        for sub_commander in bundle.sub_commanders
    } == {
        sub_commander_id: tuple(tool.name for tool in tools)
        for sub_commander_id, tools in SUB_COMMANDER_TOOLSETS.items()
    }


def test_builtin_capabilities_reference_actual_runtime_tool_names() -> None:
    expected_tool_names = {
        tool.name
        for tools in SUB_COMMANDER_TOOLSETS.values()
        for tool in tools
    }
    manifest_tool_names = {manifest.tool_name for manifest in BUILTIN_CAPABILITY_MANIFESTS}

    assert manifest_tool_names == expected_tool_names


def test_builtin_capability_metadata_distinguishes_read_and_write_surfaces() -> None:
    capability_by_id = {manifest.capability_id: manifest for manifest in BUILTIN_CAPABILITY_MANIFESTS}

    assert capability_by_id["get_tasks"].permission_class == PermissionClass.READ
    assert capability_by_id["get_tasks"].side_effect_scope == SideEffectScope.NONE
    assert capability_by_id["get_tasks"].supports_retry is True
    assert capability_by_id["get_tasks"].idempotent is True
    assert capability_by_id["get_tasks"].safe_for_parallel_use is True
    assert capability_by_id["get_tasks"].requires_confirmation is False

    assert capability_by_id["create_reminder"].permission_class == PermissionClass.WRITE
    assert capability_by_id["create_reminder"].side_effect_scope == SideEffectScope.LOCAL_STATE
    assert capability_by_id["create_reminder"].supports_retry is False
    assert capability_by_id["create_reminder"].idempotent is False
    assert capability_by_id["create_reminder"].safe_for_parallel_use is False
    assert capability_by_id["create_reminder"].requires_confirmation is True

    assert capability_by_id["web_search"].permission_class == PermissionClass.EXTERNAL
    assert capability_by_id["web_search"].side_effect_scope == SideEffectScope.NETWORK


def test_load_builtin_registry_indexes_is_cached_and_matches_bundle_indexes() -> None:
    cached = load_builtin_registry_indexes()
    rebuilt = build_registry_indexes(load_builtin_registry_bundle())

    assert cached is load_builtin_registry_indexes()
    assert cached.capability_id_by_tool_name == rebuilt.capability_id_by_tool_name
    assert cached.capability_by_id["create_reminder"].requires_confirmation is True


def test_builtin_manifests_form_a_valid_registry_bundle() -> None:
    validate_registry_bundle(
        agents=list(BUILTIN_AGENT_MANIFESTS),
        sub_commanders=list(BUILTIN_SUB_COMMANDER_MANIFESTS),
        capabilities=list(BUILTIN_CAPABILITY_MANIFESTS),
        specialist_templates=list(BUILTIN_SPECIALIST_TEMPLATE_MANIFESTS),
    )


def test_load_builtin_registry_bundle_returns_non_empty_manifest_sets() -> None:
    bundle = load_builtin_registry_bundle()

    assert bundle.agents
    assert bundle.sub_commanders
    assert bundle.capabilities
    assert isinstance(bundle.specialist_templates, tuple)


def test_build_registry_indexes_exposes_manifest_lookups_by_id() -> None:
    bundle = load_builtin_registry_bundle()

    indexes = build_registry_indexes(bundle)

    assert indexes.agent_by_id
    assert indexes.agent_by_role_value
    assert indexes.sub_commander_by_id
    assert indexes.capability_by_id
    assert isinstance(indexes.specialist_template_by_id, Mapping)
    assert set(indexes.agent_by_id) == {agent.agent_id for agent in bundle.agents}
    assert set(indexes.sub_commander_by_id) == {
        sub_commander.sub_commander_id for sub_commander in bundle.sub_commanders
    }
    assert set(indexes.capability_by_id) == {
        capability.capability_id for capability in bundle.capabilities
    }
    assert set(indexes.specialist_template_by_id) == {
        template.template_id for template in bundle.specialist_templates
    }


def test_summarize_registry_indexes_returns_read_only_debug_counts() -> None:
    bundle = load_builtin_registry_bundle()
    indexes = build_registry_indexes(bundle)

    assert summarize_registry_indexes(indexes) == {
        "agent_count": len(bundle.agents),
        "sub_commander_count": len(bundle.sub_commanders),
        "capability_count": len(bundle.capabilities),
        "specialist_template_count": len(bundle.specialist_templates),
    }


def test_build_registry_indexes_exposes_prompt_keys_skill_context_keys_and_capability_mappings() -> None:
    bundle = load_builtin_registry_bundle()

    indexes = build_registry_indexes(bundle)

    assert indexes.agent_prompt_key_by_id == {
        agent.agent_id: agent.system_prompt_key for agent in bundle.agents
    }
    assert indexes.agent_prompt_key_by_id == {
        agent.agent_id: agent.system_prompt_key for agent in BUILTIN_AGENT_MANIFESTS
    }
    assert set(indexes.agent_prompt_key_by_id.values()) == set(TOP_LEVEL_SYSTEM_PROMPTS_BY_KEY)
    assert indexes.sub_commander_prompt_key_by_id == {
        sub_commander.sub_commander_id: sub_commander.sub_commander_id
        for sub_commander in bundle.sub_commanders
    }
    assert set(indexes.sub_commander_prompt_key_by_id.values()) == {
        sub_commander.sub_commander_id for sub_commander in bundle.sub_commanders
    }
    assert indexes.skill_context_key_by_agent_id == {
        agent.agent_id: agent.skill_context_key
        for agent in bundle.agents
        if agent.skill_context_key is not None
    }
    assert indexes.capability_ids_by_sub_commander_id == {
        sub_commander.sub_commander_id: tuple(sub_commander.capability_ids)
        for sub_commander in bundle.sub_commanders
    }
    assert indexes.agent_by_role_value == {
        agent.role_value: agent for agent in bundle.agents
    }
    assert indexes.spawnable_role_values_by_agent_id == {
        agent.agent_id: tuple(agent.allowed_spawn_role_values)
        for agent in bundle.agents
        if agent.can_spawn_children and agent.allowed_spawn_role_values
    }


def test_validate_registry_bundle_accepts_loaded_builtin_registry_bundle() -> None:
    bundle = load_builtin_registry_bundle()

    validate_registry_bundle(
        agents=list(bundle.agents),
        sub_commanders=list(bundle.sub_commanders),
        capabilities=list(bundle.capabilities),
        specialist_templates=list(bundle.specialist_templates),
    )


def test_phase_one_still_declares_specialist_template_surface_even_if_runtime_is_deferred() -> None:
    assert isinstance(BUILTIN_SPECIALIST_TEMPLATE_MANIFESTS, tuple)
