"""Pytest configuration and fixtures for voicetest unit tests."""

import json
from pathlib import Path

from fastapi.testclient import TestClient
import pytest

from voicetest.container import create_container
from voicetest.engine.session import ConversationState
from voicetest.models.agent import AgentGraph
from voicetest.models.agent import AgentNode
from voicetest.models.agent import GlobalNodeSetting
from voicetest.models.agent import GoBackCondition
from voicetest.models.agent import NodeType
from voicetest.models.agent import ToolDefinition
from voicetest.models.agent import Transition
from voicetest.models.agent import TransitionCondition
from voicetest.models.results import Message
from voicetest.models.results import TestResult
from voicetest.services import SettingsService
from voicetest.services import build_app_services
from voicetest.storage.repositories import AgentRepository
from voicetest.util.cache import setup_cache_from_settings
from voicetest.web.rest import app
from voicetest.web.rest import init_storage


@pytest.fixture(autouse=True)
def fresh_container():
    """Give each test its own DI container on the FastAPI app.state.

    Mirrors the work the FastAPI lifespan handler does (container + storage init
    + cache setup) so bare `TestClient(app)` constructions get the same state
    they would in production — the lifespan only fires when the client is used
    as a context manager.
    """
    app.state.container = create_container()
    init_storage(app.state.container)
    settings = app.state.container.resolve(SettingsService).get_settings()
    setup_cache_from_settings(settings.cache)
    yield


@pytest.fixture
def container():
    """Resolve services directly for tests that construct things explicitly."""

    return create_container()


@pytest.fixture
def app_services(container):
    """AppServices bag built from the test container."""

    return build_app_services(container)


@pytest.fixture
def db_client(tmp_path, monkeypatch):
    """Create a test client with isolated database."""
    monkeypatch.setenv("VOICETEST_LINKED_AGENTS", "")

    app.state.container = create_container()
    init_storage(app.state.container)

    return TestClient(app)


@pytest.fixture
def resolved(db_client):
    """Resolve a service/repository from the db_client's container.

    Shorthand for `db_client.app.state.container.resolve(X)`.
    """

    def _resolve(cls):
        return db_client.app.state.container.resolve(cls)

    return _resolve


@pytest.fixture
def make_agent(db_client):
    """Factory: create an agent in the db_client's DB and return its record.

    Modes (mutually exclusive):
      - `config=...` — POST /api/agents (covers Retell/Vapi/etc. configs)
      - `graph=...`  — write directly via AgentRepository (covers AgentGraph)
    Returns the created agent dict (has "id" key).
    """

    def _make(name: str = "Test Agent", *, config: dict | None = None, graph=None) -> dict:
        if graph is not None:
            repo = db_client.app.state.container.resolve(AgentRepository)
            return repo.create(
                name=name,
                source_type=graph.source_type,
                graph_json=graph.model_dump_json(),
            )
        if config is not None:
            return db_client.post(
                "/api/agents",
                json={"name": name, "config": config},
            ).json()
        raise ValueError("make_agent requires either config= or graph=")

    return _make


@pytest.fixture
def platform_client(tmp_path, monkeypatch):
    """Create a test client with isolated database, cleared API keys, and temp settings dir."""
    monkeypatch.setenv("VOICETEST_LINKED_AGENTS", "")

    monkeypatch.delenv("RETELL_API_KEY", raising=False)
    monkeypatch.delenv("VAPI_API_KEY", raising=False)
    monkeypatch.delenv("LIVEKIT_API_KEY", raising=False)
    monkeypatch.delenv("LIVEKIT_API_SECRET", raising=False)

    monkeypatch.chdir(tmp_path)
    (tmp_path / ".voicetest").mkdir()

    app.state.container = create_container()
    init_storage(app.state.container)

    return TestClient(app)


# fixtures_dir is inherited from tests/conftest.py


@pytest.fixture
def retell_fixtures_dir(fixtures_dir: Path) -> Path:
    """Return path to Retell test fixtures."""
    return fixtures_dir / "retell"


@pytest.fixture
def sample_retell_config(retell_fixtures_dir: Path) -> dict:
    """Load sample Retell configuration."""
    config_path = retell_fixtures_dir / "sample_config.json"
    return json.loads(config_path.read_text())


@pytest.fixture
def sample_retell_config_path(retell_fixtures_dir: Path) -> Path:
    """Return path to sample Retell configuration file."""
    return retell_fixtures_dir / "sample_config.json"


# ---------------------------------------------------------------------------
# Fixtures for the call-transcript import + replay feature
# ---------------------------------------------------------------------------


@pytest.fixture
def retell_call():
    """Factory fixture: build a minimal Retell call object.

    Usage:
        retell_call()                     # default call_id="call_001"
        retell_call("call_a")             # custom id
        retell_call("call_a", duration_ms=999)  # override any field
    """

    def _build(call_id: str = "call_001", **overrides) -> dict:
        payload = {
            "call_id": call_id,
            "agent_id": "agent_xyz",
            "call_type": "phone_call",
            "call_status": "ended",
            "start_timestamp": 1700000000000,
            "end_timestamp": 1700000060000,
            "duration_ms": 60000,
            "transcript_object": [
                {"role": "agent", "content": "Hi, how can I help?"},
                {"role": "user", "content": "I need to cancel."},
            ],
        }
        payload.update(overrides)
        return payload

    return _build


@pytest.fixture
def imported_test_result():
    """Factory fixture: build a TestResult with status='imported'."""

    def _build(name: str = "call_001", *, transcript: list | None = None) -> TestResult:
        return TestResult(
            test_name=name,
            status="imported",
            transcript=transcript
            or [
                Message(role="assistant", content="Hi"),
                Message(role="user", content="Hello"),
            ],
            turn_count=2,
            duration_ms=12000,
            end_reason="ended",
        )

    return _build


@pytest.fixture
def stub_conversation_runner(monkeypatch):
    """Stub ConversationRunner.run so replay-flow tests don't need a real graph
    or LLM. The stub drains the user simulator into a transcript with placeholder
    agent replies, then returns a populated ConversationState.

    Returns a list that captures each simulator instance the runner was invoked
    with — useful for asserting the right scripted simulator was constructed.
    """

    captured_simulators: list = []

    async def fake_run(self, test_case, simulator, **kwargs):
        captured_simulators.append(simulator)
        transcript = []
        while True:
            response = await simulator.generate(transcript)
            if response is None:
                break
            transcript.append(Message(role="user", content=response.message))
            transcript.append(Message(role="assistant", content="agent reply"))
        state = ConversationState()
        state.transcript = transcript
        state.turn_count = len(transcript) // 2
        state.end_reason = "ended"
        return state

    monkeypatch.setattr("voicetest.engine.session.ConversationRunner.run", fake_run)
    return captured_simulators


@pytest.fixture
def sample_test_cases(retell_fixtures_dir: Path) -> list[dict]:
    """Load sample test cases."""
    tests_path = retell_fixtures_dir / "sample_tests.json"
    return json.loads(tests_path.read_text())


@pytest.fixture
def sample_retell_llm_config(retell_fixtures_dir: Path) -> dict:
    """Load sample Retell LLM configuration."""
    config_path = retell_fixtures_dir / "sample_llm_config.json"
    return json.loads(config_path.read_text())


@pytest.fixture
def sample_retell_llm_config_path(retell_fixtures_dir: Path) -> Path:
    """Return path to sample Retell LLM configuration file."""
    return retell_fixtures_dir / "sample_llm_config.json"


@pytest.fixture
def sample_retell_llm_dashboard_export(retell_fixtures_dir: Path) -> dict:
    """Load sample Retell LLM dashboard export (wrapped in retellLlmData)."""
    config_path = retell_fixtures_dir / "sample_llm_dashboard_export.json"
    return json.loads(config_path.read_text())


@pytest.fixture
def sample_retell_llm_dashboard_export_path(retell_fixtures_dir: Path) -> Path:
    """Return path to sample Retell LLM dashboard export file."""
    return retell_fixtures_dir / "sample_llm_dashboard_export.json"


@pytest.fixture
def sample_retell_config_complex(retell_fixtures_dir: Path) -> dict:
    """Load complex Retell Conversation Flow configuration with tools."""
    config_path = retell_fixtures_dir / "sample_config_complex.json"
    return json.loads(config_path.read_text())


@pytest.fixture
def sample_retell_config_complex_path(retell_fixtures_dir: Path) -> Path:
    """Return path to complex Retell configuration file."""
    return retell_fixtures_dir / "sample_config_complex.json"


@pytest.fixture
def sample_retell_config_global_nodes(retell_fixtures_dir: Path) -> dict:
    """Load Retell CF configuration with global nodes (pizza ordering)."""
    config_path = retell_fixtures_dir / "sample_config_global_nodes.json"
    return json.loads(config_path.read_text())


@pytest.fixture
def sample_retell_config_logic_split(retell_fixtures_dir: Path) -> dict:
    """Load Retell CF configuration with a logic split node (no instruction)."""
    config_path = retell_fixtures_dir / "sample_config_logic_split.json"
    return json.loads(config_path.read_text())


@pytest.fixture
def sample_retell_config_logic_split_path(retell_fixtures_dir: Path) -> Path:
    """Return path to Retell CF configuration with logic split node."""
    return retell_fixtures_dir / "sample_config_logic_split.json"


@pytest.fixture
def sample_retell_config_extract_variables(retell_fixtures_dir: Path) -> dict:
    """Load Retell CF configuration with extract_dynamic_variables node."""
    config_path = retell_fixtures_dir / "sample_config_extract_variables.json"
    return json.loads(config_path.read_text())


@pytest.fixture
def vapi_fixtures_dir(fixtures_dir: Path) -> Path:
    """Return path to VAPI test fixtures."""
    return fixtures_dir / "vapi"


@pytest.fixture
def sample_vapi_assistant(vapi_fixtures_dir: Path) -> dict:
    """Load sample VAPI assistant configuration."""
    config_path = vapi_fixtures_dir / "sample_assistant.json"
    return json.loads(config_path.read_text())


@pytest.fixture
def sample_vapi_assistant_path(vapi_fixtures_dir: Path) -> Path:
    """Return path to sample VAPI assistant file."""
    return vapi_fixtures_dir / "sample_assistant.json"


@pytest.fixture
def sample_vapi_assistant_simple(vapi_fixtures_dir: Path) -> dict:
    """Load simple VAPI assistant configuration."""
    config_path = vapi_fixtures_dir / "sample_assistant_simple.json"
    return json.loads(config_path.read_text())


@pytest.fixture
def sample_vapi_squad(vapi_fixtures_dir: Path) -> dict:
    """Load sample VAPI squad configuration."""
    config_path = vapi_fixtures_dir / "sample_squad.json"
    return json.loads(config_path.read_text())


@pytest.fixture
def sample_vapi_squad_path(vapi_fixtures_dir: Path) -> Path:
    """Return path to sample VAPI squad file."""
    return vapi_fixtures_dir / "sample_squad.json"


@pytest.fixture
def livekit_fixtures_dir(fixtures_dir: Path) -> Path:
    """Return path to LiveKit test fixtures."""
    return fixtures_dir / "livekit"


@pytest.fixture
def sample_livekit_agent_path(livekit_fixtures_dir: Path) -> Path:
    """Return path to sample LiveKit agent Python file."""
    return livekit_fixtures_dir / "sample_agent.py"


@pytest.fixture
def sample_livekit_agent_code(sample_livekit_agent_path: Path) -> str:
    """Load sample LiveKit agent Python code."""
    return sample_livekit_agent_path.read_text()


@pytest.fixture
def sample_livekit_simple_path(livekit_fixtures_dir: Path) -> Path:
    """Return path to simple LiveKit agent file."""
    return livekit_fixtures_dir / "sample_agent_simple.py"


@pytest.fixture
def sample_livekit_simple_code(sample_livekit_simple_path: Path) -> str:
    """Load simple LiveKit agent Python code."""
    return sample_livekit_simple_path.read_text()


# Common AgentGraph fixtures for testing


@pytest.fixture
def simple_graph():
    """Simple two-node graph with greeting and farewell nodes."""

    return AgentGraph(
        nodes={
            "greeting": AgentNode(
                id="greeting",
                state_prompt="Greet the user warmly.",
                transitions=[
                    Transition(
                        target_node_id="farewell",
                        condition=TransitionCondition(
                            type="llm_prompt", value="User wants to leave"
                        ),
                    )
                ],
            ),
            "farewell": AgentNode(
                id="farewell",
                state_prompt="Say goodbye politely.",
                transitions=[],
            ),
        },
        entry_node_id="greeting",
        source_type="custom",
    )


@pytest.fixture
def single_node_graph():
    """Single-node graph for basic testing."""

    return AgentGraph(
        nodes={
            "main": AgentNode(
                id="main",
                state_prompt="You are a helpful assistant.",
                transitions=[],
            ),
        },
        entry_node_id="main",
        source_type="custom",
    )


@pytest.fixture
def graph_with_tools():
    """Graph with tools attached to nodes."""

    lookup_tool = ToolDefinition(
        name="lookup_user",
        description="Look up user in database",
        parameters={
            "type": "object",
            "properties": {
                "user_id": {"type": "string", "description": "User ID"},
            },
            "required": ["user_id"],
        },
    )
    end_call_tool = ToolDefinition(
        name="end_call",
        description="End the call",
        parameters={},
    )

    return AgentGraph(
        nodes={
            "greeting": AgentNode(
                id="greeting",
                state_prompt="Greet the user.",
                tools=[end_call_tool],
                transitions=[
                    Transition(
                        target_node_id="lookup",
                        condition=TransitionCondition(
                            type="llm_prompt", value="User wants to check their account"
                        ),
                    )
                ],
            ),
            "lookup": AgentNode(
                id="lookup",
                state_prompt="Look up the user's account.",
                tools=[lookup_tool, end_call_tool],
                transitions=[],
            ),
        },
        entry_node_id="greeting",
        source_type="custom",
    )


@pytest.fixture
def multi_node_graph():
    """Multi-node graph with branching transitions."""

    return AgentGraph(
        nodes={
            "greeting": AgentNode(
                id="greeting",
                state_prompt="Greet the customer warmly and ask how you can help.",
                transitions=[
                    Transition(
                        target_node_id="billing",
                        condition=TransitionCondition(
                            type="llm_prompt", value="User has billing question"
                        ),
                    ),
                    Transition(
                        target_node_id="support",
                        condition=TransitionCondition(
                            type="llm_prompt", value="User needs technical support"
                        ),
                    ),
                ],
            ),
            "billing": AgentNode(
                id="billing",
                state_prompt="Help the customer with billing inquiries.",
                transitions=[
                    Transition(
                        target_node_id="end",
                        condition=TransitionCondition(type="llm_prompt", value="Billing resolved"),
                    )
                ],
            ),
            "support": AgentNode(
                id="support",
                state_prompt="Provide technical support.",
                transitions=[
                    Transition(
                        target_node_id="end",
                        condition=TransitionCondition(type="llm_prompt", value="Support complete"),
                    )
                ],
            ),
            "end": AgentNode(
                id="end",
                state_prompt="Thank the customer and end the call politely.",
                transitions=[],
            ),
        },
        entry_node_id="greeting",
        source_type="custom",
        source_metadata={"general_prompt": "You are a professional customer service agent."},
    )


@pytest.fixture
def graph_with_metadata():
    """Graph with source metadata set."""

    return AgentGraph(
        nodes={
            "main": AgentNode(
                id="main",
                state_prompt="You are a helpful assistant.",
                transitions=[],
            ),
        },
        entry_node_id="main",
        source_type="retell",
        source_metadata={
            "general_prompt": "You are a professional assistant.",
            "llm_id": "llm_test123",
            "model": "gpt-4o",
        },
    )


@pytest.fixture
def graph_with_global_node():
    """Graph with a single global node (cancel request)."""

    return AgentGraph(
        nodes={
            "greeting": AgentNode(
                id="greeting",
                state_prompt="Greet the caller and ask what pizza they want.",
                transitions=[
                    Transition(
                        target_node_id="customize",
                        condition=TransitionCondition(
                            type="llm_prompt", value="Caller stated their pizza choice"
                        ),
                    )
                ],
            ),
            "customize": AgentNode(
                id="customize",
                state_prompt="Confirm size, crust, and toppings.",
                transitions=[
                    Transition(
                        target_node_id="confirm",
                        condition=TransitionCondition(
                            type="llm_prompt", value="All details collected"
                        ),
                    )
                ],
            ),
            "confirm": AgentNode(
                id="confirm",
                state_prompt="Read back the order and ask to confirm.",
                transitions=[],
            ),
            "cancel_request": AgentNode(
                id="cancel_request",
                state_prompt="Ask if they are sure they want to cancel.",
                transitions=[],
                global_node_setting=GlobalNodeSetting(
                    condition="Caller wants to cancel or hang up",
                    go_back_conditions=[
                        GoBackCondition(
                            id="go-back-cancel-1",
                            condition=TransitionCondition(
                                type="llm_prompt",
                                value="Caller changes their mind and wants to continue",
                            ),
                        ),
                    ],
                ),
            ),
        },
        entry_node_id="greeting",
        source_type="retell",
        source_metadata={"general_prompt": "You are a pizza ordering assistant."},
    )


@pytest.fixture
def graph_with_multiple_global_nodes():
    """Graph with two global nodes (cancel + specials)."""

    return AgentGraph(
        nodes={
            "greeting": AgentNode(
                id="greeting",
                state_prompt="Greet the caller.",
                transitions=[
                    Transition(
                        target_node_id="order",
                        condition=TransitionCondition(
                            type="llm_prompt", value="Caller wants to order"
                        ),
                    )
                ],
            ),
            "order": AgentNode(
                id="order",
                state_prompt="Take the order.",
                transitions=[],
            ),
            "cancel_request": AgentNode(
                id="cancel_request",
                state_prompt="Ask if they want to cancel.",
                transitions=[],
                global_node_setting=GlobalNodeSetting(
                    condition="Caller wants to cancel or hang up",
                    go_back_conditions=[
                        GoBackCondition(
                            id="go-back-cancel-1",
                            condition=TransitionCondition(
                                type="llm_prompt",
                                value="Caller wants to continue",
                            ),
                        ),
                    ],
                ),
            ),
            "ask_specials": AgentNode(
                id="ask_specials",
                state_prompt="Tell the caller about specials.",
                transitions=[],
                global_node_setting=GlobalNodeSetting(
                    condition="Caller asks about specials or deals",
                    go_back_conditions=[
                        GoBackCondition(
                            id="go-back-specials-1",
                            condition=TransitionCondition(
                                type="llm_prompt",
                                value="Caller heard the specials and wants to continue",
                            ),
                        ),
                    ],
                ),
            ),
        },
        entry_node_id="greeting",
        source_type="retell",
        source_metadata={"general_prompt": "Pizza ordering assistant."},
    )


@pytest.fixture
def graph_with_global_no_go_back():
    """Graph where the global node has no go-back conditions (forward only)."""

    return AgentGraph(
        nodes={
            "greeting": AgentNode(
                id="greeting",
                state_prompt="Greet the caller.",
                transitions=[
                    Transition(
                        target_node_id="order",
                        condition=TransitionCondition(
                            type="llm_prompt", value="Caller wants to order"
                        ),
                    )
                ],
            ),
            "order": AgentNode(
                id="order",
                state_prompt="Take the order.",
                transitions=[],
            ),
            "emergency_end": AgentNode(
                id="emergency_end",
                state_prompt="End the call immediately.",
                node_type=NodeType.END,
                global_node_setting=GlobalNodeSetting(
                    condition="Caller reports an emergency",
                ),
            ),
        },
        entry_node_id="greeting",
        source_type="retell",
    )


@pytest.fixture
def graph_with_dynamic_variables(fixtures_dir: Path):
    """Graph with {{variable}} placeholders in prompts."""

    config_path = fixtures_dir / "graphs" / "graph_with_dynamic_variables.json"
    return AgentGraph.model_validate_json(config_path.read_text())


@pytest.fixture
def graph_dry_analysis(fixtures_dir: Path):
    """Graph with repeated and similar prompts for DRY analysis testing."""

    config_path = fixtures_dir / "graphs" / "graph_dry_analysis.json"
    return AgentGraph.model_validate_json(config_path.read_text())
