"""Load and validate YAML test fixtures into TestCase objects."""

from __future__ import annotations

from pathlib import Path

import yaml

from hatchdx.harness.models import TestCase, TestExpectation


class FixtureError(Exception):
    """Raised when a fixture file is invalid."""


REQUIRED_TEST_FIELDS = {"name", "tool", "input", "expect"}


def load_fixtures(directory: Path) -> list[TestCase]:
    """Load all *.yaml files from *directory* and return a list of TestCases.

    Raises FixtureError with a clear message when a file is malformed.
    """
    directory = Path(directory)
    if not directory.is_dir():
        raise FixtureError(f"Fixtures directory does not exist: {directory}")

    yaml_files = sorted(directory.glob("*.yaml"))
    if not yaml_files:
        return []

    test_cases: list[TestCase] = []
    for filepath in yaml_files:
        test_cases.extend(_load_file(filepath))
    return test_cases


def _load_file(filepath: Path) -> list[TestCase]:
    """Parse a single YAML fixture file into TestCase objects."""
    try:
        raw = yaml.safe_load(filepath.read_text())
    except yaml.YAMLError as exc:
        raise FixtureError(f"Invalid YAML in {filepath.name}: {exc}") from exc
    except (UnicodeDecodeError, PermissionError, OSError) as exc:
        raise FixtureError(f"Cannot read {filepath.name}: {exc}") from exc

    # Empty file → no tests
    if raw is None:
        return []

    if not isinstance(raw, dict) or "tests" not in raw:
        raise FixtureError(
            f"{filepath.name}: expected a top-level 'tests' key with a list of test cases, "
            f"got {type(raw).__name__}.\n"
            "  Example structure:\n"
            "    tests:\n"
            '      - name: "my test"\n'
            '        tool: "my_tool"\n'
            "        input: {}\n"
            "        expect:\n"
            '          status: "success"'
        )

    tests = raw["tests"]
    if not isinstance(tests, list):
        raise FixtureError(
            f"{filepath.name}: 'tests' must be a list, got {type(tests).__name__}"
        )

    return [_parse_test_case(entry, filepath) for entry in tests]


def _parse_test_case(entry: dict, filepath: Path) -> TestCase:
    """Validate and convert a raw dict into a TestCase."""
    if not isinstance(entry, dict):
        raise FixtureError(
            f"{filepath.name}: each test must be a mapping, got {type(entry).__name__}"
        )

    missing = REQUIRED_TEST_FIELDS - entry.keys()
    if missing:
        test_name = entry.get("name", "<unnamed>")
        raise FixtureError(
            f"{filepath.name}: test '{test_name}' is missing fields: "
            f"{', '.join(sorted(missing))}"
        )

    expect_raw = entry["expect"]
    if not isinstance(expect_raw, dict):
        raise FixtureError(
            f"{filepath.name}: test '{entry['name']}' expect must be a mapping"
        )

    expectation = TestExpectation(
        status=expect_raw.get("status"),
        is_error=expect_raw.get("is_error"),
        schema=expect_raw.get("schema"),
        content_contains=expect_raw.get("content_contains"),
        content_not_contains=expect_raw.get("content_not_contains"),
        max_latency_ms=expect_raw.get("max_latency_ms"),
    )

    return TestCase(
        name=entry["name"],
        tool=entry["tool"],
        input=entry["input"] if entry["input"] is not None else {},
        expect=expectation,
    )