"""Shared pytest fixtures and configuration for the test suite.

This module provides:
- OS-specific test markers (windows_only, macos_only, posix_only, linux_only)
- Shared fixtures for CLI testing and configuration management
- Test utilities for ANSI stripping and traceback preservation
"""

from __future__ import annotations

import re
import sys
from collections.abc import Callable, Iterator
from dataclasses import fields
from datetime import UTC, datetime
from pathlib import Path

import pytest
from click.testing import CliRunner

import lib_cli_exit_tools
from check_zpools.models import CheckResult, PoolHealth, PoolStatus, Severity

# ============================================================================
# OS Detection Constants
# ============================================================================

IS_WINDOWS = sys.platform == "win32"
IS_MACOS = sys.platform == "darwin"
IS_LINUX = sys.platform.startswith("linux")
IS_POSIX = not IS_WINDOWS


# Load .env file for integration tests
def _load_dotenv() -> None:
    """Load .env file if it exists for integration test configuration."""
    try:
        from dotenv import load_dotenv

        env_file = Path(__file__).parent.parent / ".env"
        if env_file.exists():
            load_dotenv(env_file)
    except ImportError:
        # python-dotenv not installed, skip loading
        pass


_load_dotenv()

ANSI_ESCAPE_PATTERN = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]")
CONFIG_FIELDS: tuple[str, ...] = tuple(field.name for field in fields(type(lib_cli_exit_tools.config)))


# ============================================================================
# Pytest Hooks - OS-Specific Test Markers
# ============================================================================


def pytest_configure(config: pytest.Config) -> None:
    """Register custom markers for OS-specific and integration tests."""
    config.addinivalue_line(
        "markers",
        "windows_only: mark test to run only on Windows",
    )
    config.addinivalue_line(
        "markers",
        "macos_only: mark test to run only on macOS",
    )
    config.addinivalue_line(
        "markers",
        "linux_only: mark test to run only on Linux",
    )
    config.addinivalue_line(
        "markers",
        "posix_only: mark test to run only on POSIX systems (Linux, macOS, Unix)",
    )
    config.addinivalue_line(
        "markers",
        "os_agnostic: mark test as platform-independent (runs everywhere)",
    )
    config.addinivalue_line(
        "markers",
        "integration: mark test as integration test (tests multiple components)",
    )
    config.addinivalue_line(
        "markers",
        "slow: mark test as slow-running (typically >1 second)",
    )


def pytest_runtest_setup(item: pytest.Item) -> None:
    """Skip tests based on OS-specific markers.

    This hook runs before each test and checks if the test should be
    skipped based on the current operating system. Tests without OS markers
    are assumed to be OS-agnostic and run on all platforms.
    """
    marker_names = {mark.name for mark in item.iter_markers()}

    # Check for Windows-only tests
    if "windows_only" in marker_names and not IS_WINDOWS:
        pytest.skip("Test requires Windows")

    # Check for macOS-only tests
    if "macos_only" in marker_names and not IS_MACOS:
        pytest.skip("Test requires macOS")

    # Check for Linux-only tests
    if "linux_only" in marker_names and not IS_LINUX:
        pytest.skip("Test requires Linux")

    # Check for POSIX-only tests
    if "posix_only" in marker_names and not IS_POSIX:
        pytest.skip("Test requires POSIX system (Linux, macOS, or Unix)")


def _remove_ansi_codes(text: str) -> str:
    """Return ``text`` stripped of ANSI escape sequences.

    Why
        Tests compare human-readable CLI output; stripping colour codes keeps
        assertions stable across environments.

    Inputs
        text:
            Raw string captured from CLI output.

    Outputs
        str: The string without ANSI escape sequences.
    """

    return ANSI_ESCAPE_PATTERN.sub("", text)


def _snapshot_cli_config() -> dict[str, object]:
    """Capture every attribute from ``lib_cli_exit_tools.config``.

    Why
        Tests toggle traceback behaviour; keeping a snapshot lets fixtures
        restore the configuration after each run.

    Outputs
        dict[str, object]: Mapping of attribute names to their original values.
    """

    return {name: getattr(lib_cli_exit_tools.config, name) for name in CONFIG_FIELDS}


def _restore_cli_config(snapshot: dict[str, object]) -> None:
    """Reapply the previously captured CLI configuration.

    Why
        Ensures global state looks untouched to subsequent tests.

    Inputs
        snapshot:
            Mapping generated by :func:`_snapshot_cli_config`.
    """

    for name, value in snapshot.items():
        setattr(lib_cli_exit_tools.config, name, value)


@pytest.fixture
def cli_runner() -> CliRunner:
    """Provide a fresh :class:`CliRunner` per test."""

    return CliRunner()


@pytest.fixture
def strip_ansi() -> Callable[[str], str]:
    """Return a helper that strips ANSI escape sequences from a string."""

    def _strip(value: str) -> str:
        return _remove_ansi_codes(value)

    return _strip


@pytest.fixture
def preserve_traceback_state() -> Iterator[None]:
    """Snapshot and restore the entire ``lib_cli_exit_tools`` configuration."""

    snapshot = _snapshot_cli_config()
    try:
        yield
    finally:
        _restore_cli_config(snapshot)


@pytest.fixture
def isolated_traceback_config(monkeypatch: pytest.MonkeyPatch) -> None:
    """Reset traceback flags to a known baseline before each test."""

    lib_cli_exit_tools.reset_config()
    monkeypatch.setattr(lib_cli_exit_tools.config, "traceback", False, raising=False)
    monkeypatch.setattr(lib_cli_exit_tools.config, "traceback_force_color", False, raising=False)


# ============================================================================
# Domain Model Fixtures - Reusable Test Data
# ============================================================================


@pytest.fixture
def healthy_pool_status() -> PoolStatus:
    """Create a healthy ZFS pool status for testing.

    Why
        Represents a real, healthy pool used across multiple tests.
        Shared fixture eliminates duplication and ensures consistency.
        Used as baseline for happy-path tests.

    Returns
        Pool with ONLINE health, 50% capacity, no errors.
    """
    return PoolStatus(
        name="rpool",
        health=PoolHealth.ONLINE,
        capacity_percent=50.0,
        size_bytes=1024**4,  # 1 TB
        allocated_bytes=int(0.5 * 1024**4),  # 500 GB
        free_bytes=int(0.5 * 1024**4),  # 500 GB
        read_errors=0,
        write_errors=0,
        checksum_errors=0,
        last_scrub=datetime(2025, 11, 24, 12, 0, 0, tzinfo=UTC),
        scrub_errors=0,
        scrub_in_progress=False,
    )


@pytest.fixture
def ok_check_result(healthy_pool_status: PoolStatus) -> CheckResult:
    """Create a check result indicating all pools are healthy.

    Why
        Represents successful monitoring outcome - all checks passed.
        Shared fixture eliminates duplication and ensures consistency.
        Used for testing happy paths and success scenarios.

    Returns
        CheckResult with OK severity and one healthy pool.
    """
    return CheckResult(
        timestamp=datetime(2025, 11, 24, 12, 0, 0, tzinfo=UTC),
        pools=[healthy_pool_status],
        issues=[],
        overall_severity=Severity.OK,
    )


@pytest.fixture
def configurable_pool_status():
    """Create a factory for building pool status with configurable parameters.

    Why
        Allows tests to easily create pool statuses with different configurations
        (capacity, scrub status, etc.) without duplicating pool creation logic.
        Particularly useful for alerting and monitoring tests.

    Returns
        Factory function that creates PoolStatus instances.

    Example
        pool = configurable_pool_status(name="data", capacity=85.0)
        pool_with_scrub = configurable_pool_status(scrub_in_progress=True)
    """

    def _create_pool(
        name: str = "rpool",
        capacity: float = 50.0,
        scrub_in_progress: bool = False,
        last_scrub: datetime | None = None,
    ) -> PoolStatus:
        """Create a configurable pool status for testing.

        Inputs
            name: Pool name (default: "rpool")
            capacity: Capacity percentage (default: 50.0)
            scrub_in_progress: Whether scrub is running (default: False)
            last_scrub: Last scrub timestamp (default: current time)

        Outputs
            PoolStatus with specified configuration
        """
        if last_scrub is None:
            last_scrub = datetime.now(UTC)

        return PoolStatus(
            name=name,
            health=PoolHealth.ONLINE,
            capacity_percent=capacity,
            size_bytes=1024**4,
            allocated_bytes=int((capacity / 100.0) * 1024**4),
            free_bytes=int(((100.0 - capacity) / 100.0) * 1024**4),
            read_errors=0,
            write_errors=0,
            checksum_errors=0,
            last_scrub=last_scrub,
            scrub_errors=0,
            scrub_in_progress=scrub_in_progress,
        )

    return _create_pool
