"""Session-attach fixture coverage for execution-architecture runtime rules.

Spec refs:
- docs/proposals/execution-architecture-refactor.md#tests-required/session-runtime-tests
- docs/proposals/execution-architecture-refactor.md#execution-contract-specific-design-rules/lifecycle-semantics
- docs/proposals/execution-architecture-refactor.md#execution-contract-specific-design-rules/persona-provenance-and-subagent-inheritance
"""

from __future__ import annotations

from pathlib import Path

from returns.result import Failure, Success

from anima.app.session_service import (
    ChatAttachRequest,
    DefaultSessionService,
    SessionProjectionPort,
    TalkAttachRequest,
)
from anima.core.contracts import SessionTransportState
from anima.core.execution import ExecutionSessionState, ResolvedExecutionContract
from anima.core.schemas import AgentState
from anima.core.schemas.models import AgentStateData
from anima.shell.state import (
    read_session_projection,
    write_session_projection,
    write_state,
)
from tests.fixtures.session_attach import (
    make_daemon_explicit_attachability_fixture,
    make_parent_spawnability_cases,
    make_session_attach_matrix_cases,
    make_talk_chat_parity_rules,
)
from tests.fixtures.execution_contract_resolution import (
    make_consumer_conformance_fixture,
)


def test_attach_matrix_covers_running_and_terminal_state_socket_transport_cross_product() -> (
    None
):
    cases = make_session_attach_matrix_cases()

    # 4 lifecycle states x 2 socket states x 2 transport states
    assert len(cases) == 16

    signatures = {
        (case.state, case.socket_present, case.transport_present) for case in cases
    }
    assert len(signatures) == 16


def test_attach_matrix_prefers_terminal_state_over_transport_failures() -> None:
    terminal_states = {AgentState.completed, AgentState.failed, AgentState.killed}
    cases = make_session_attach_matrix_cases()

    terminal_cases = [case for case in cases if case.state in terminal_states]
    assert terminal_cases
    assert all(case.expected_signal == "terminal_state" for case in terminal_cases)


def test_attach_matrix_running_branch_covers_socket_then_transport_then_attach() -> (
    None
):
    running_cases = [
        case
        for case in make_session_attach_matrix_cases()
        if case.state == AgentState.running
    ]
    expected = {
        (False, False): "socket_missing",
        (False, True): "socket_missing",
        (True, False): "transport_missing",
        (True, True): "attach_ok",
    }
    observed = {
        (case.socket_present, case.transport_present): case.expected_signal
        for case in running_cases
    }
    assert observed == expected


def test_daemon_explicit_fixture_remains_attachable_across_multi_no_tool_turns() -> (
    None
):
    fixture = make_daemon_explicit_attachability_fixture(no_tool_turn_count=4)

    assert fixture.lifecycle == "daemon"
    assert fixture.completion_policy == "explicit"
    assert fixture.expected_attachable is True
    assert len(fixture.no_tool_turn_prompts) == 4
    assert all("no tools" in prompt for prompt in fixture.no_tool_turn_prompts)


def test_talk_chat_parity_rules_cover_shared_terminal_and_socket_semantics() -> None:
    rules = {rule.rule_id: rule for rule in make_talk_chat_parity_rules()}

    assert "terminal-state-outranks-transport-failure" in rules
    assert (
        rules["terminal-state-outranks-transport-failure"].expected_signal
        == "terminal_state"
    )
    assert "running-missing-socket-is-socket-error" in rules
    assert rules["running-missing-socket-is-socket-error"].expected_signal == (
        "socket_missing"
    )


def test_parent_spawnability_cases_cover_materialized_success_or_structured_failure() -> (
    None
):
    cases = make_parent_spawnability_cases()

    assert len(cases) == 4

    grouped = {}
    for case in cases:
        grouped.setdefault(case.parent_source, []).append(case)

    for source in ("inline", "synthesized"):
        source_cases = grouped[source]
        assert any(
            case.expected_outcome == "materialized_child_success"
            for case in source_cases
        )
        failure_cases = [
            case
            for case in source_cases
            if case.expected_outcome == "spawnability_error"
        ]
        assert len(failure_cases) == 1
        assert failure_cases[0].expected_error_code == (
            f"{source}_persona_requires_materialization"
        )


class _StubProjectionPort(SessionProjectionPort):
    def __init__(
        self,
        *,
        mutable_state: AgentStateData,
        contract: ResolvedExecutionContract,
        session_state: ExecutionSessionState,
    ) -> None:
        self._mutable_state = mutable_state
        self._contract = contract
        self._session_state = session_state

    def read_state(self, instance_id: str):
        _ = instance_id
        return Success(self._mutable_state)

    def load_contract(self, instance_id: str):
        _ = instance_id
        return Success(self._contract)

    def load_session_state(self, instance_id: str):
        _ = instance_id
        return Success(self._session_state)


def _build_service_case(
    *,
    state: AgentState,
    socket_path: str,
    transport_connected: bool,
) -> DefaultSessionService:
    contract = make_consumer_conformance_fixture(
        instance_id="inst_attach_service",
    ).contract
    mutable_state = AgentStateData(
        instance_id="inst_attach_service",
        trace_id="tr_attach_service",
        persona_name="attach-service",
        state=state,
        mcp_socket=socket_path,
    )
    session_state = ExecutionSessionState(
        instance_id="inst_attach_service",
        transport=SessionTransportState(
            attach_mode="interactive",
            transport_connected=transport_connected,
            socket_path=socket_path,
        ),
    )
    return DefaultSessionService(
        projection_port=_StubProjectionPort(
            mutable_state=mutable_state,
            contract=contract,
            session_state=session_state,
        )
    )


def test_session_service_terminal_state_outranks_missing_socket() -> None:
    for terminal_state in (AgentState.completed, AgentState.failed, AgentState.killed):
        service = _build_service_case(
            state=terminal_state,
            socket_path="/tmp/does-not-exist.sock",
            transport_connected=False,
        )
        projection_result = service.project_talk_attach(
            TalkAttachRequest(instance_id="inst_attach_service")
        )

        assert isinstance(projection_result, Success)
        projection = projection_result.unwrap()
        assert projection.presentation.kind == "terminal_state"
        assert projection.presentation.terminal_state == terminal_state
        assert projection.presentation.transport_failure is not None
        assert projection.presentation.transport_failure.code == "socket_not_found"


def test_session_projection_fallback_resolves_relative_socket_path(
    tmp_path: Path,
    monkeypatch,
) -> None:
    state_root = tmp_path / "state"
    monkeypatch.setenv("ANIMA_STATE", str(state_root))
    socket_path = state_root / "agents" / "inst_relative_socket" / "mcp.sock"
    socket_path.parent.mkdir(parents=True, exist_ok=True)
    socket_path.touch()

    state_data = AgentStateData(
        instance_id="inst_relative_socket",
        trace_id="tr_relative_socket",
        persona_name="relative-socket",
        state=AgentState.running,
        mcp_socket="agents/inst_relative_socket/mcp.sock",
    )
    assert write_state(state_data.instance_id, state_data) == Success(None)

    projection_result = read_session_projection("inst_relative_socket")
    assert isinstance(projection_result, Success)
    projection = projection_result.unwrap()
    assert projection.transport.socket_path == str(socket_path)
    assert projection.transport.transport_connected is True


def test_session_projection_write_supports_clear_and_cas_conflict(
    tmp_path: Path,
    monkeypatch,
) -> None:
    state_root = tmp_path / "state"
    monkeypatch.setenv("ANIMA_STATE", str(state_root))
    state_data = AgentStateData(
        instance_id="inst_projection_cas",
        trace_id="tr_projection_cas",
        persona_name="projection-cas",
        state=AgentState.running,
        mcp_socket="mcp.sock",
    )
    assert write_state(state_data.instance_id, state_data) == Success(None)

    first = ExecutionSessionState(
        instance_id="inst_projection_cas",
        transport=SessionTransportState(
            attach_mode="interactive",
            transport_connected=True,
            socket_path="mcp.sock",
        ),
    )
    assert write_session_projection("inst_projection_cas", first) == Success("v1")

    cleared = ExecutionSessionState(
        instance_id="inst_projection_cas",
        transport=SessionTransportState(
            attach_mode=None,
            transport_connected=False,
            socket_path="mcp.sock",
            terminal_snapshot_ref=None,
        ),
    )
    assert write_session_projection(
        "inst_projection_cas",
        cleared,
        expected_version=1,
    ) == Success("v2")

    loaded = read_session_projection("inst_projection_cas")
    assert isinstance(loaded, Success)
    assert loaded.unwrap().transport.attach_mode is None

    cas_conflict = write_session_projection(
        "inst_projection_cas",
        cleared,
        expected_version=1,
    )
    assert isinstance(cas_conflict, Failure)
    assert "CAS conflict" in str(cas_conflict.failure())


def test_talk_and_chat_attach_paths_use_shared_session_service() -> None:
    import inspect

    from anima.shell.cli_commands import chat_cmd, talk_cmd

    talk_source = inspect.getsource(talk_cmd.talk_impl)
    chat_source = inspect.getsource(chat_cmd._wait_for_socket_or_terminal)

    assert "DefaultSessionService" in talk_source
    assert "project_talk_attach" in talk_source
    assert "DefaultSessionService" in chat_source
    assert "project_chat_attach" in chat_source
