"""
Story test fixtures.

Provides:
- HA container with demo state (session-scoped)
- FastMCP in-memory client for setup/teardown
- Story catalog discovery and loading
"""

from __future__ import annotations

import asyncio
import logging
import os
import shutil
import sys
import tempfile
from collections.abc import AsyncGenerator
from pathlib import Path

import pytest
import yaml
from testcontainers.core.container import DockerContainer

# Add src to path for imports
TESTS_DIR = Path(__file__).resolve().parent.parent.parent
REPO_ROOT = TESTS_DIR.parent
sys.path.insert(0, str(REPO_ROOT / "src"))
sys.path.insert(0, str(TESTS_DIR))

from fastmcp import Client  # noqa: E402
from test_constants import HA_TEST_IMAGE, TEST_TOKEN  # noqa: E402
from uat._inprocess import inprocess_mcp_client  # noqa: E402
from uat.ha_wait import wait_for_ha_ready  # noqa: E402

logger = logging.getLogger(__name__)

HA_IMAGE = HA_TEST_IMAGE

CATALOG_DIR = Path(__file__).parent / "catalog"


# ---------------------------------------------------------------------------
# Story loading
# ---------------------------------------------------------------------------
def discover_stories() -> list[dict]:
    """Discover all story YAML files in the catalog directory."""
    stories = []
    for yaml_file in sorted(CATALOG_DIR.glob("s*.yaml")):
        with open(yaml_file) as f:
            story = yaml.safe_load(f)
        story["_file"] = str(yaml_file)
        stories.append(story)
    return stories


def story_ids() -> list[str]:
    """Return story IDs for pytest parametrize."""
    return [s["id"] for s in discover_stories()]


# ---------------------------------------------------------------------------
# HA Container (session-scoped)
# ---------------------------------------------------------------------------
def _setup_config_directory() -> Path:
    """Copy initial_test_state to a temp dir for the HA container."""
    config_dir = Path(tempfile.mkdtemp(prefix="ha_story_"))
    initial_state = TESTS_DIR / "initial_test_state"
    if not initial_state.exists():
        raise FileNotFoundError(f"initial_test_state not found at {initial_state}")

    shutil.copytree(initial_state, config_dir, dirs_exist_ok=True)

    # Set permissions
    os.chmod(config_dir, 0o755)
    for item in config_dir.rglob("*"):
        if item.is_file():
            os.chmod(item, 0o644)
        elif item.is_dir():
            os.chmod(item, 0o755)

    return config_dir


@pytest.fixture(scope="session")
def ha_container():
    """Session-scoped HA container for all story tests."""
    config_dir = _setup_config_directory()

    container = (
        DockerContainer(HA_IMAGE)
        .with_exposed_ports(8123)
        .with_volume_mapping(str(config_dir), "/config", "rw")
        .with_env("TZ", "UTC")
        .with_kwargs(privileged=True)
    )

    with container:
        port = container.get_exposed_port(8123)
        url = f"http://localhost:{port}"
        logger.info(f"HA container started on {url}")

        # Set env for server
        os.environ["HOMEASSISTANT_URL"] = url
        os.environ["HOMEASSISTANT_TOKEN"] = TEST_TOKEN

        wait_for_ha_ready(url, TEST_TOKEN)

        yield {"url": url, "token": TEST_TOKEN, "port": port}

    # Cleanup
    shutil.rmtree(config_dir, ignore_errors=True)


@pytest.fixture(scope="session")
def event_loop():
    """Create an event loop for the test session."""
    loop = asyncio.new_event_loop()
    yield loop
    loop.close()


# ---------------------------------------------------------------------------
# FastMCP client for setup/teardown
# ---------------------------------------------------------------------------
@pytest.fixture
async def mcp_client(ha_container) -> AsyncGenerator[Client]:
    """FastMCP in-memory client for programmatic setup/teardown."""
    async with inprocess_mcp_client(
        ha_container["url"], ha_container["token"]
    ) as client:
        yield client


# ---------------------------------------------------------------------------
# Story execution helpers
# ---------------------------------------------------------------------------
async def run_setup_steps(mcp_client: Client, steps: list[dict]) -> None:
    """Execute setup steps via FastMCP in-memory calls."""
    for step in steps:
        tool_name = step["tool"]
        args = step.get("args", {})
        logger.info(f"  [setup] {tool_name}({args})")
        try:
            await mcp_client.call_tool(tool_name, args)
        except Exception as e:
            logger.error(f"  [setup] {tool_name} failed: {e}")
            raise


async def run_teardown_steps(mcp_client: Client, steps: list[dict]) -> None:
    """Execute teardown steps via FastMCP in-memory calls."""
    for step in steps:
        tool_name = step["tool"]
        args = step.get("args", {})
        logger.info(f"  [teardown] {tool_name}({args})")
        try:
            await mcp_client.call_tool(tool_name, args)
        except Exception as e:
            logger.warning(f"  [teardown] {tool_name} failed (ok): {e}")
