"""Shared pytest fixtures for the unfamiliar test suite.

Provides fixtures for:
- In-memory SQLite database connections
- Repository instances
- Sample campaign data
- Tree-sitter parsers for TS/JS
"""

from __future__ import annotations

import contextlib
import multiprocessing
import sqlite3
from collections import namedtuple
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch

import pytest
from hypothesis import HealthCheck, settings

from unfamiliar.analysis.models import (
    FileInfo,
    ParsedFile,
)
from unfamiliar.constants import CampaignStatus
from unfamiliar.db.connection import init_database
from unfamiliar.db.repositories import (
    CampaignRepository,
    ChainRepository,
    FileSnapshotRepository,
    MissionFileRefRepository,
    MissionLinkRepository,
    MissionRepository,
    ModuleRepository,
)
from unfamiliar.models import Campaign
from unfamiliar.tui.themes.film_noir.theme import FILM_NOIR_THEME
from unfamiliar.tui.themes.registry import ThemeRegistry

if TYPE_CHECKING:
    from tree_sitter import Node

    from unfamiliar.analysis.models import ParsedImport


with contextlib.suppress(RuntimeError):
    multiprocessing.set_start_method("fork")

# Suppress differing_executors health check globally (needed for mutmut)
settings.register_profile(
    "mutmut", suppress_health_check=[HealthCheck.differing_executors]
)
settings.load_profile("mutmut")

Repos = namedtuple(
    "Repos",
    [
        "campaign_repo",
        "module_repo",
        "mission_repo",
        "mission_file_ref_repo",
        "mission_link_repo",
        "chain_repo",
        "file_snapshot_repo",
    ],
)


@pytest.fixture
def theme_registry() -> ThemeRegistry:
    """Theme registry with Film Noir theme registered."""
    registry = ThemeRegistry()
    registry.register(FILM_NOIR_THEME)
    return registry


@pytest.fixture
def db_conn():
    """Create an in-memory SQLite database with schema initialized.

    Yields:
        sqlite3.Connection: Connection with foreign keys enabled and
            row_factory set to sqlite3.Row.
    """
    conn = sqlite3.connect(":memory:")
    conn.execute("PRAGMA foreign_keys = ON")
    conn.row_factory = sqlite3.Row
    init_database(conn)
    yield conn
    conn.close()


@pytest.fixture
def mock_checkpointer():
    """Fixture that patches _checkpoint_context and yields the checkpointer mock"""

    with patch(
        "unfamiliar.llm.missions.pipeline._checkpoint_context"
    ) as mock_checkpointer:
        mock_checkpointer.return_value.__enter__.return_value = MagicMock()
        mock_checkpointer.return_value.__exit__.return_value = False
        yield mock_checkpointer.return_value.__enter__.return_value


@pytest.fixture
def all_repos(db_conn):
    """Create all repository instances for testing.

    Returns:
        namedtuple with campaign_repo, module_repo, mission_repo,
        mission_file_ref_repo, mission_link_repo, chain_repo,
        and file_snapshot_repo.
    """
    return Repos(
        campaign_repo=CampaignRepository(db_conn),
        module_repo=ModuleRepository(db_conn),
        mission_repo=MissionRepository(db_conn),
        mission_file_ref_repo=MissionFileRefRepository(db_conn),
        mission_link_repo=MissionLinkRepository(db_conn),
        chain_repo=ChainRepository(db_conn),
        file_snapshot_repo=FileSnapshotRepository(db_conn),
    )


@pytest.fixture
def sample_campaign(all_repos):
    """Create a minimal valid Campaign for testing.

    Returns:
        Campaign: Created campaign with id populated.
    """
    campaign = Campaign(
        repo_path="/tmp/test-repo",
        status=CampaignStatus.INITIALIZING,
    )
    return all_repos.campaign_repo.create(campaign)


# --- Tree-sitter Parser Fixtures ---


@pytest.fixture
def ts_parser():
    """Create a tree-sitter Parser with TypeScript language.

    Yields:
        Parser: Configured for TypeScript parsing.
    """
    import tree_sitter_typescript as tsts
    from tree_sitter import Language, Parser

    language = Language(tsts.language_typescript())
    parser = Parser(language)
    yield parser


@pytest.fixture
def js_parser():
    """Create a tree-sitter Parser with JavaScript language.

    Yields:
        Parser: Configured for JavaScript parsing.
    """
    import tree_sitter_javascript as tsjs
    from tree_sitter import Language, Parser

    language = Language(tsjs.language())
    parser = Parser(language)
    yield parser


# --- Helper Functions for Tree-sitter Tests ---


def parse_ts(source: str, ts_parser) -> Node:
    """Parse TypeScript source string and return root node.

    Args:
        source: TypeScript source code as string.
        ts_parser: Tree-sitter parser fixture.

    Returns:
        Node: Root node of the parse tree.
    """
    return ts_parser.parse(source.encode()).root_node


def parse_js(source: str, js_parser) -> Node:
    """Parse JavaScript source string and return root node.

    Args:
        source: JavaScript source code as string.
        js_parser: Tree-sitter parser fixture.

    Returns:
        Node: Root node of the parse tree.
    """
    return js_parser.parse(source.encode()).root_node


# --- Other Helpers ---


def make_file_info(
    path: str,
    size_bytes: int = 100,
    content_hash: str = "abc123",
    language: str = "python",
) -> FileInfo:
    """Create a FileInfo dict for testing."""
    return FileInfo(
        path=path,
        language=language,
        size_bytes=size_bytes,
        content_hash=content_hash,
    )


def make_parsed_file(
    path: str,
    complexity: int = 0,
    imports: list[ParsedImport] | None = None,
) -> ParsedFile:
    """Create a ParsedFile dict for testing."""
    return ParsedFile(
        path=path,
        language="python",
        imports=imports or [],
        definitions=[],
        entry_points=[],
        complexity=complexity,
        line_count=10,
    )


# LLM Test Fixtures and Helpers


def make_analysis_result(**overrides):
    """Create a minimal valid AnalysisResult with sensible defaults.

    All nested structures (ImportGraph, TestInfo, SecurityResult) are
    initialized with empty defaults suitable for unit testing.

    Args:
        **overrides: Any field to override from defaults.

    Returns:
        AnalysisResult with empty lists and stub nested objects.
    """
    from unfamiliar.analysis.models import (
        AnalysisResult,
        FileTestInfo,
        ImportGraph,
        SecurityResult,
    )

    defaults = {
        "files": [],
        "parsed_files": {},
        "import_graph": ImportGraph(),
        "modules": [],
        "test_info": FileTestInfo(),
        "scored_files": [],
        "entry_points": [],
        "languages": {},
        "repo_path": "/tmp/test-repo",
        "security": SecurityResult(),
    }
    defaults.update(overrides)
    return AnalysisResult(**defaults)


def make_briefing_result(**overrides):
    """Create a minimal valid BriefingResult with BriefingResponse and
     ModuleDescription list.

    Args:
        **overrides: Any field to override from defaults.

    Returns:
        BriefingResult with a valid BriefingResponse and empty modules list.
    """
    from unfamiliar.llm.briefing.generator import BriefingResult
    from unfamiliar.llm.briefing.schemas import (
        BriefingResponse,
    )

    default_briefing = BriefingResponse(
        summary="Test codebase summary.",
        tech_stack=["Python"],
        security_posture="No significant security concerns.",
        modules=[],
    )
    defaults = {
        "briefing": default_briefing,
        "modules": [],
        "context_tokens_estimate": 100,
    }
    defaults.update(overrides)
    return BriefingResult(**defaults)


def make_mission_item(**overrides):
    """Create a minimal valid MissionItemResponse with sensible defaults.

    Defaults:
        - title/title_plain: "Test Mission"
        - difficulty: "quick"
        - prerequisite_titles: [] (root mission)
        - file_refs: []
        - security_tag: False
        - module_index: 0

    Args:
        **overrides: Any field to override from defaults.

    Returns:
        MissionItemResponse with valid minimal values.
    """
    from unfamiliar.llm.missions.schemas import (
        MissionItemResponse,
    )

    defaults = {
        "title": "Test Mission",
        "title_plain": "Test Mission",
        "description": "A test mission description.",
        "difficulty": "quick",
        "completion_condition": "You understand the test mission.",
        "security_tag": False,
        "standard_tag": None,
        "narrative_intro": "The test case begins...",
        "narrative_outro": "Another test completed.",
        "file_refs": [],
        "module_index": 0,
        "prerequisite_titles": [],
    }
    defaults.update(overrides)
    return MissionItemResponse(**defaults)


def make_category_response(
    missions: list | None = None,
    chains: list | None = None,
):
    """Create a CategoryMissionsResponse wrapping the given missions and chains.

    Args:
        missions: List of MissionItemResponse. Defaults to single minimal mission.
        chains: List of ChainResponse. Defaults to empty list.

    Returns:
        CategoryMissionsResponse with provided missions and chains.
    """
    from unfamiliar.llm.missions.schemas import CategoryMissionsResponse

    if missions is None:
        missions = [make_mission_item()]
    if chains is None:
        chains = []

    return CategoryMissionsResponse(missions=missions, chains=chains)


def mock_mission_chain(response):
    """Context manager that mocks the LangChain chain for mission generation.

    Similar to mock_briefing_chain but for the mission generation pipeline.

    Usage:
        with mock_mission_chain(my_response) as mock_chain:
            result = generate_category_missions(...)

    Args:
        response: The CategoryMissionsResponse to return from chain.invoke().

    Returns:
        Context manager that yields the mock chain.
    """
    from contextlib import contextmanager
    from unittest.mock import MagicMock, patch

    @contextmanager
    def _mock_context():
        mock_chain = MagicMock()
        mock_chain.invoke.return_value = response

        with patch("unfamiliar.llm.missions.generator.MISSION_PROMPT") as mock_prompt:
            mock_prompt.__or__ = MagicMock(return_value=mock_chain)
            yield mock_chain

    return _mock_context()


def sample_config():
    """Create a minimal UnfamiliarConfig for testing.

    Returns:
        UnfamiliarConfig with provider="anthropic" and empty api_keys.
    """
    from unfamiliar.config import UnfamiliarConfig

    return UnfamiliarConfig(
        provider="anthropic",
        token_budget=10000,
        theme="film_noir",
    )
