"""Context-policy verification commands for M5 Robot E2E tests."""

from __future__ import annotations

from types import SimpleNamespace
from typing import Any, cast
from unittest.mock import MagicMock

from helper_m5_e2e_support import _create_project, _fail, _setup_db

from cleveragents.application.services.context_phase_analysis import (
    analyze_phase_summaries,
)
from cleveragents.application.services.context_tiers import ContextTierService
from cleveragents.application.services.execute_phase_context_assembler import (
    ACMSExecutePhaseContextAssembler,
)
from cleveragents.application.services.llm_actors import LLMExecuteActor
from cleveragents.application.services.plan_executor import StrategyDecision
from cleveragents.cli.commands.project_context import _read_policy, _write_policy
from cleveragents.config.settings import Settings
from cleveragents.domain.models.acms.tiers import ContextTier, TieredFragment
from cleveragents.domain.models.core.context_policy import (
    VALID_PHASES,
    ContextView,
    ProjectContextPolicy,
)
from cleveragents.domain.models.core.project import ContextConfig, NamespacedProject


def context_tier_config() -> None:
    """Verify ContextConfig with hot/warm/cold tier management."""
    default_cfg = ContextConfig()
    if default_cfg.hot_max_tokens is not None:
        _fail(f"default hot_max_tokens should be None: {default_cfg.hot_max_tokens}")
    if default_cfg.warm_max_decisions is not None:
        _fail(
            f"default warm_max_decisions should be None: "
            f"{default_cfg.warm_max_decisions}"
        )
    if default_cfg.cold_max_decisions is not None:
        _fail(
            f"default cold_max_decisions should be None: "
            f"{default_cfg.cold_max_decisions}"
        )

    tier_cfg = ContextConfig(
        hot_max_tokens=128_000,
        warm_max_decisions=50,
        cold_max_decisions=200,
        max_file_size=2_000_000,
        max_total_size=100_000_000,
    )
    if tier_cfg.hot_max_tokens != 128_000:
        _fail(f"hot_max_tokens mismatch: {tier_cfg.hot_max_tokens}")
    if tier_cfg.warm_max_decisions != 50:
        _fail(f"warm_max_decisions mismatch: {tier_cfg.warm_max_decisions}")
    if tier_cfg.cold_max_decisions != 200:
        _fail(f"cold_max_decisions mismatch: {tier_cfg.cold_max_decisions}")
    if tier_cfg.max_file_size != 2_000_000:
        _fail(f"max_file_size mismatch: {tier_cfg.max_file_size}")
    if tier_cfg.max_total_size != 100_000_000:
        _fail(f"max_total_size mismatch: {tier_cfg.max_total_size}")

    proj = NamespacedProject(
        name="tier-test",
        namespace="local",
        context_config=tier_cfg,
    )
    if proj.context_config.hot_max_tokens != 128_000:
        _fail("project did not preserve hot_max_tokens")
    if proj.context_config.warm_max_decisions != 50:
        _fail("project did not preserve warm_max_decisions")
    if proj.context_config.cold_max_decisions != 200:
        _fail("project did not preserve cold_max_decisions")

    print("m5-context-tier-config-ok")


def context_policy_set_show() -> None:
    """Set a context policy via SQL helpers and read it back."""
    proj_repo, _link_repo, _svc, sf = _setup_db()
    _create_project(proj_repo, "local/policy-test")

    policy = ProjectContextPolicy(
        default_view=ContextView(
            include_resources=["local/repo-*"],
            exclude_paths=["*.log", "*.tmp"],
            max_file_size=1_048_576,
            max_total_size=52_428_800,
        ),
        strategize_view=ContextView(
            include_resources=["local/core-*"],
            include_paths=["src/**/*.py"],
            max_file_size=500_000,
        ),
    )

    _write_policy(sf, "local/policy-test", policy)
    loaded = _read_policy(sf, "local/policy-test")

    if loaded.default_view.include_resources != ["local/repo-*"]:
        _fail(
            f"default include_resources mismatch: "
            f"{loaded.default_view.include_resources}"
        )
    if loaded.default_view.exclude_paths != ["*.log", "*.tmp"]:
        _fail(f"default exclude_paths mismatch: {loaded.default_view.exclude_paths}")
    if loaded.default_view.max_file_size != 1_048_576:
        _fail(f"default max_file_size mismatch: {loaded.default_view.max_file_size}")
    if loaded.strategize_view is None:
        _fail("strategize_view is None after round-trip")
    if loaded.strategize_view.include_resources != ["local/core-*"]:
        _fail(
            f"strategize include_resources mismatch: "
            f"{loaded.strategize_view.include_resources}"
        )
    if loaded.strategize_view.include_paths != ["src/**/*.py"]:
        _fail(
            f"strategize include_paths mismatch: {loaded.strategize_view.include_paths}"
        )
    if loaded.execute_view is not None:
        _fail("execute_view should be None")
    if loaded.apply_view is not None:
        _fail("apply_view should be None")

    print("m5-context-policy-set-show-ok")


def acms_phase_inheritance() -> None:
    """Verify ACMS view inheritance: default -> strategize -> execute -> apply."""
    policy = ProjectContextPolicy(
        default_view=ContextView(
            include_resources=["*"],
            max_total_size=50_000_000,
        ),
        strategize_view=ContextView(
            include_resources=["core-*"],
            max_file_size=1_000_000,
        ),
    )

    rv_default = policy.resolve_view("default")
    if rv_default.include_resources != ["*"]:
        _fail(f"default resolution wrong: {rv_default.include_resources}")
    if rv_default.max_total_size != 50_000_000:
        _fail(f"default max_total_size wrong: {rv_default.max_total_size}")

    rv_strat = policy.resolve_view("strategize")
    if rv_strat.include_resources != ["core-*"]:
        _fail(f"strategize resolution wrong: {rv_strat.include_resources}")
    if rv_strat.max_file_size != 1_000_000:
        _fail(f"strategize max_file_size wrong: {rv_strat.max_file_size}")

    rv_exec = policy.resolve_view("execute")
    if rv_exec.include_resources != ["core-*"]:
        _fail(f"execute should inherit strategize: {rv_exec.include_resources}")

    rv_apply = policy.resolve_view("apply")
    if rv_apply.include_resources != ["core-*"]:
        _fail(f"apply should inherit via chain: {rv_apply.include_resources}")

    policy2 = ProjectContextPolicy(
        default_view=ContextView(include_resources=["*"]),
        strategize_view=ContextView(include_resources=["core-*"]),
        execute_view=ContextView(
            include_resources=["exec-*"],
            exclude_paths=["tests/**"],
        ),
    )
    rv_apply2 = policy2.resolve_view("apply")
    if rv_apply2.include_resources != ["exec-*"]:
        _fail(f"apply should inherit from execute: {rv_apply2.include_resources}")
    if rv_apply2.exclude_paths != ["tests/**"]:
        _fail(f"apply exclude_paths wrong: {rv_apply2.exclude_paths}")

    print("m5-acms-phase-inheritance-ok")


def acms_scoped_context() -> None:
    """Verify ACMS produces scoped context output per phase."""
    policy = ProjectContextPolicy(
        default_view=ContextView(
            include_resources=["*"],
            max_file_size=2_000_000,
            max_total_size=100_000_000,
        ),
        strategize_view=ContextView(
            include_resources=["local/large-repo"],
            include_paths=["src/**/*.py", "docs/**/*.md"],
            exclude_paths=["**/test_*.py"],
            max_file_size=1_000_000,
        ),
        execute_view=ContextView(
            include_resources=["local/large-repo"],
            include_paths=["src/**/*.py"],
            exclude_paths=["**/test_*.py", "**/conftest.py"],
            max_file_size=500_000,
            max_total_size=1_200_000,
        ),
        apply_view=ContextView(
            include_resources=["local/large-repo"],
            include_paths=["src/**/*.py"],
            exclude_paths=["docs/**", "**/test_*.py", "**/conftest.py"],
            max_file_size=250_000,
            max_total_size=700_000,
        ),
    )

    for phase in ("default", "strategize", "execute", "apply"):
        rv = policy.resolve_view(phase)
        expected_attr = getattr(policy, f"{phase}_view")
        if expected_attr is None:
            _fail(f"phase {phase} view should not be None")
        if rv.include_resources != expected_attr.include_resources:
            _fail(
                f"{phase} include_resources mismatch: "
                f"{rv.include_resources} != {expected_attr.include_resources}"
            )

    strat_view = policy.resolve_view("strategize")
    exec_view = policy.resolve_view("execute")
    apply_view = policy.resolve_view("apply")

    if strat_view.max_file_size is None:
        _fail("strategize max_file_size is None")
    if exec_view.max_file_size is None:
        _fail("execute max_file_size is None")
    if apply_view.max_file_size is None:
        _fail("apply max_file_size is None")
    if not (
        strat_view.max_file_size > exec_view.max_file_size > apply_view.max_file_size
    ):
        _fail(
            f"size limits should narrow: "
            f"{strat_view.max_file_size} > {exec_view.max_file_size} > "
            f"{apply_view.max_file_size}"
        )

    sample_fragments = [
        TieredFragment(
            fragment_id="f-core",
            content="x" * 900,
            tier=ContextTier.HOT,
            resource_id="local/large-repo",
            project_name="local/scoped-test",
            token_count=900,
            metadata={"path": "src/core.py", "byte_size": 900_000},
        ),
        TieredFragment(
            fragment_id="f-engine",
            content="x" * 500,
            tier=ContextTier.HOT,
            resource_id="local/large-repo",
            project_name="local/scoped-test",
            token_count=500,
            metadata={"path": "src/engine.py", "byte_size": 450_000},
        ),
        TieredFragment(
            fragment_id="f-test",
            content="x" * 120,
            tier=ContextTier.WARM,
            resource_id="local/large-repo",
            project_name="local/scoped-test",
            token_count=120,
            metadata={"path": "src/test_core.py", "byte_size": 100_000},
        ),
        TieredFragment(
            fragment_id="f-doc",
            content="x" * 180,
            tier=ContextTier.WARM,
            resource_id="local/large-repo",
            project_name="local/scoped-test",
            token_count=180,
            metadata={"path": "docs/design.md", "byte_size": 200_000},
        ),
        TieredFragment(
            fragment_id="f-conftest",
            content="x" * 90,
            tier=ContextTier.COLD,
            resource_id="local/large-repo",
            project_name="local/scoped-test",
            token_count=90,
            metadata={"path": "src/conftest.py", "byte_size": 80_000},
        ),
        TieredFragment(
            fragment_id="f-huge",
            content="x" * 1500,
            tier=ContextTier.COLD,
            resource_id="local/large-repo",
            project_name="local/scoped-test",
            token_count=1500,
            metadata={"path": "src/huge.py", "byte_size": 1_500_000},
        ),
    ]
    analysis = analyze_phase_summaries(policy, sample_fragments, budget_tokens=2_000)

    for phase in ("strategize", "execute", "apply"):
        phase_row = analysis["phases"][phase]
        if not phase_row["summary"]:
            _fail(f"{phase} summary should be non-empty")
        if "resource_count" not in phase_row or "total_bytes" not in phase_row:
            _fail(f"{phase} summary missing resource/size metrics")

    narrowing = analysis["narrowing"]
    if not narrowing["overall"]:
        _fail(f"narrowing should hold across phases: {narrowing}")
    proj_repo, _link_repo, _svc, sf = _setup_db()
    _create_project(proj_repo, "local/scoped-test")
    _write_policy(sf, "local/scoped-test", policy)
    loaded = _read_policy(sf, "local/scoped-test")

    for phase in ("default", "strategize", "execute", "apply"):
        rv_loaded = loaded.resolve_view(phase)
        rv_orig = policy.resolve_view(phase)
        if rv_loaded.include_resources != rv_orig.include_resources:
            _fail(f"{phase} round-trip include_resources mismatch")
        if rv_loaded.max_file_size != rv_orig.max_file_size:
            _fail(f"{phase} round-trip max_file_size mismatch")

    print("m5-acms-scoped-context-ok")


def context_policy_clear() -> None:
    """Verify clearing a phase view causes inheritance fallback."""
    policy = ProjectContextPolicy(
        default_view=ContextView(include_resources=["all-*"]),
        strategize_view=ContextView(include_resources=["strat-*"]),
        execute_view=ContextView(include_resources=["exec-*"]),
        apply_view=ContextView(include_resources=["apply-*"]),
    )

    rv = policy.resolve_view("execute")
    if rv.include_resources != ["exec-*"]:
        _fail(f"before clear, execute wrong: {rv.include_resources}")

    cleared = policy.model_copy(update={"execute_view": None})
    rv_after = cleared.resolve_view("execute")
    if rv_after.include_resources != ["strat-*"]:
        _fail(
            f"after clear, execute should inherit strategize: "
            f"{rv_after.include_resources}"
        )

    rv_apply = cleared.resolve_view("apply")
    if rv_apply.include_resources != ["apply-*"]:
        _fail(f"apply should still be its own: {rv_apply.include_resources}")

    cleared2 = cleared.model_copy(update={"apply_view": None})
    rv_apply2 = cleared2.resolve_view("apply")
    if rv_apply2.include_resources != ["strat-*"]:
        _fail(
            f"after clearing apply+execute, apply should inherit strategize: "
            f"{rv_apply2.include_resources}"
        )

    proj_repo, _link_repo, _svc, sf = _setup_db()
    _create_project(proj_repo, "local/clear-test")
    _write_policy(sf, "local/clear-test", cleared2)
    loaded = _read_policy(sf, "local/clear-test")

    if loaded.execute_view is not None:
        _fail("execute_view should be None after round-trip")
    if loaded.apply_view is not None:
        _fail("apply_view should be None after round-trip")

    rv_loaded = loaded.resolve_view("execute")
    if rv_loaded.include_resources != ["strat-*"]:
        _fail(f"round-trip execute fallback wrong: {rv_loaded.include_resources}")

    print("m5-context-policy-clear-ok")


def context_view_validation() -> None:
    """Verify ContextView validation rules."""
    cv = ContextView(max_file_size=1, max_total_size=1)
    if cv.max_file_size != 1:
        _fail(f"max_file_size should be 1: {cv.max_file_size}")

    try:
        ContextView(max_file_size=0)
        _fail("max_file_size=0 should be rejected")
    except ValueError:
        pass

    try:
        ContextView(max_total_size=-1)
        _fail("max_total_size=-1 should be rejected")
    except ValueError:
        pass

    cv_none = ContextView(max_file_size=None, max_total_size=None)
    if cv_none.max_file_size is not None:
        _fail("max_file_size should be None")

    expected_phases = {"default", "strategize", "execute", "apply"}
    if expected_phases != VALID_PHASES:
        _fail(f"VALID_PHASES mismatch: {VALID_PHASES} != {expected_phases}")

    policy = ProjectContextPolicy()
    try:
        policy.resolve_view("invalid-phase")
        _fail("resolve_view should reject invalid phase")
    except ValueError:
        pass

    print("m5-context-view-validation-ok")


def llm_execute_acms_context() -> None:
    """Verify execute-phase LLM prompt uses ACMS assembled execute context."""
    proj_repo, _link_repo, _svc, sf = _setup_db()
    _create_project(proj_repo, "local/exec-ctx")

    policy = ProjectContextPolicy(
        default_view=ContextView(include_paths=["**/*"]),
        strategize_view=ContextView(include_paths=["docs/**"]),
        execute_view=ContextView(
            include_paths=["src/**"],
            max_file_size=100_000,
            max_total_size=100_000,
        ),
    )
    _write_policy(sf, "local/exec-ctx", policy)

    tier_service = ContextTierService(settings=Settings())
    tier_service.store(
        TieredFragment(
            fragment_id="frag-docs",
            content="documentation fragment",
            tier=ContextTier.HOT,
            resource_id="local/large-repo",
            project_name="local/exec-ctx",
            token_count=20,
            metadata={"path": "docs/readme.md", "relevance_score": "0.6"},
        )
    )
    tier_service.store(
        TieredFragment(
            fragment_id="frag-src",
            content="def run():\n    return 'ok'",
            tier=ContextTier.HOT,
            resource_id="local/large-repo",
            project_name="local/exec-ctx",
            token_count=16,
            metadata={"path": "src/main.py", "relevance_score": "0.9"},
        )
    )

    assembler = ACMSExecutePhaseContextAssembler(
        context_tier_service=tier_service,
        project_repository=proj_repo,
        hot_max_tokens=1024,
    )

    mock_llm = MagicMock()
    mock_llm.invoke.return_value = SimpleNamespace(
        content="FILE: src/out.py\n```python\nprint('ok')\n```\n"
    )
    registry = cast(Any, SimpleNamespace(create_llm=MagicMock(return_value=mock_llm)))
    plan = SimpleNamespace(
        identity=SimpleNamespace(plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV"),
        action_name="local/action",
        project_links=[SimpleNamespace(project_name="local/exec-ctx")],
    )
    action = SimpleNamespace(execution_actor="openai/gpt-4")
    lifecycle = SimpleNamespace(
        get_plan=MagicMock(return_value=plan),
        get_action=MagicMock(return_value=action),
    )

    actor = LLMExecuteActor(
        provider_registry=registry,
        lifecycle_service=lifecycle,
        context_assembler=assembler,
    )
    actor.execute(
        plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV",
        decisions=[
            StrategyDecision(
                decision_id="d1",
                step_text="Implement feature",
                sequence=0,
                parent_id=None,
            )
        ],
        stream_callback=None,
    )

    call_args = mock_llm.invoke.call_args
    if call_args is None:
        _fail("LLM invoke was not called")
    prompt = call_args.args[0][0].content
    if "ACMS Execute-Phase Context" not in prompt:
        _fail("prompt missing ACMS execute-phase context section")
    if "src/main.py" not in prompt:
        _fail("prompt missing execute-view source fragment")
    if "docs/readme.md" in prompt:
        _fail("prompt should exclude strategize-only docs fragment in execute view")
    print("m5-llm-execute-acms-context-ok")
