"""Pytest JSON parser for test results.

This parser extracts test results from pytest JSON format files
(generated by pytest-json-report or similar plugins).
Uses the shared reference_config infrastructure for configurable patterns.
"""

from __future__ import annotations

import json
from collections.abc import Iterator
from pathlib import Path
from typing import TYPE_CHECKING, Any

from elspais.graph.parsers import ParseContext, ParsedContent
from elspais.utilities.reference_config import (
    ReferenceConfig,
    ReferenceResolver,
    extract_ids_from_text,
)
from elspais.utilities.test_identity import build_test_id_from_nodeid, build_test_id_from_result

if TYPE_CHECKING:
    from elspais.utilities.patterns import PatternConfig


class PytestJSONParser:
    """Parser for Pytest JSON test result files.

    Parses JSON output from pytest-json-report or similar pytest plugins.

    Uses configurable patterns from ReferenceConfig for:
    - Separator characters (- _ etc.)
    - Case sensitivity
    - Prefix requirements

    Also implements the LineClaimingParser protocol via ``claim_and_parse()``
    so it can be used in the standard ParserRegistry pipeline.
    """

    priority = 90

    def __init__(
        self,
        pattern_config: PatternConfig | None = None,
        reference_resolver: ReferenceResolver | None = None,
        base_path: Path | None = None,
    ) -> None:
        """Initialize PytestJSONParser with optional configuration.

        Args:
            pattern_config: Configuration for ID structure. If None, uses defaults.
            reference_resolver: Resolver for file-specific reference config. If None,
                               uses default ReferenceConfig.
            base_path: Base path for resolving file-specific configs.
        """
        self._pattern_config = pattern_config
        self._reference_resolver = reference_resolver
        self._base_path = base_path or Path(".")

    def _get_pattern_config(self) -> PatternConfig:
        """Get pattern config from instance or create default.

        Returns:
            PatternConfig to use for parsing.
        """
        if self._pattern_config is not None:
            return self._pattern_config

        from elspais.utilities.patterns import PatternConfig

        return PatternConfig.from_dict(
            {
                "prefix": "REQ",
                "types": {
                    "prd": {"id": "p", "name": "PRD"},
                    "ops": {"id": "o", "name": "OPS"},
                    "dev": {"id": "d", "name": "DEV"},
                },
                "id_format": {"style": "numeric", "digits": 5},
            }
        )

    def _get_reference_config(self, source_file: str | None = None) -> ReferenceConfig:
        """Get reference config for the current file.

        Args:
            source_file: Optional source file path for file-specific config.

        Returns:
            ReferenceConfig for parsing.
        """
        if self._reference_resolver is not None and source_file:
            return self._reference_resolver.resolve(Path(source_file), self._base_path)

        if self._reference_resolver is not None:
            return self._reference_resolver.defaults

        return ReferenceConfig()

    def parse(self, content: str, source_path: str) -> list[dict[str, Any]]:
        """Parse Pytest JSON content and return test result dicts.

        Args:
            content: JSON file content.
            source_path: Path to the source file.

        Returns:
            List of test result dictionaries with keys:
            - id: Unique test ID
            - name: Test name
            - classname: Test class/module name
            - status: passed, failed, skipped, or error
            - duration: Test duration in seconds
            - message: Error/failure message (if any)
            - validates: List of requirement IDs this test validates
        """
        results: list[dict[str, Any]] = []

        try:
            data = json.loads(content)
        except json.JSONDecodeError:
            return results

        # Handle pytest-json-report format
        if "tests" in data:
            for test in data["tests"]:
                result = self._parse_pytest_json_report_test(test, source_path)
                if result:
                    results.append(result)
        # Handle simpler format with just a list of tests
        elif isinstance(data, list):
            for test in data:
                result = self._parse_simple_test(test, source_path)
                if result:
                    results.append(result)

        return results

    def _parse_pytest_json_report_test(
        self, test: dict[str, Any], source_path: str
    ) -> dict[str, Any] | None:
        """Parse a test from pytest-json-report format.

        Args:
            test: Test dict from pytest-json-report.
            source_path: Path to the source file.

        Returns:
            Parsed test result dict or None.
        """
        nodeid = test.get("nodeid", "")
        outcome = test.get("outcome", "passed")

        # Parse nodeid to get module and test name
        # Format: path/to/test.py::TestClass::test_method
        # or: path/to/test.py::test_function
        parts = nodeid.split("::")
        if len(parts) >= 2:
            classname = parts[0]
            name = "::".join(parts[1:])
        else:
            classname = ""
            name = nodeid

        # Map outcome to status
        status_map = {
            "passed": "passed",
            "failed": "failed",
            "skipped": "skipped",
            "error": "error",
            "xfailed": "skipped",  # Expected failure
            "xpassed": "passed",  # Unexpected pass
        }
        status = status_map.get(outcome, "passed")

        # Get duration
        duration = 0.0
        if "duration" in test:
            duration = float(test["duration"])
        elif "call" in test and "duration" in test["call"]:
            duration = float(test["call"]["duration"])

        # Get message from call or setup/teardown
        message = None
        for phase in ["call", "setup", "teardown"]:
            if phase in test:
                phase_data = test[phase]
                if isinstance(phase_data, dict):
                    if "longrepr" in phase_data:
                        message = str(phase_data["longrepr"])[:200]
                        break
                    elif "message" in phase_data:
                        message = str(phase_data["message"])[:200]
                        break

        # Extract requirement references
        validates = self._extract_req_ids(f"{classname} {name}", source_path)

        # Generate canonical TEST node ID from nodeid (already has file path)
        test_id = build_test_id_from_nodeid(nodeid)

        return {
            "id": f"{source_path}::{nodeid}",
            "name": name,
            "classname": classname,
            "status": status,
            "duration": duration,
            "message": message,
            "validates": validates,
            "source_path": source_path,
            "test_id": test_id,
        }

    def _parse_simple_test(self, test: dict[str, Any], source_path: str) -> dict[str, Any] | None:
        """Parse a test from simple list format.

        Args:
            test: Test dict with name, status, etc.
            source_path: Path to the source file.

        Returns:
            Parsed test result dict or None.
        """
        name = test.get("name", "")
        if not name:
            return None

        classname = test.get("classname", test.get("module", ""))
        status = test.get("status", test.get("outcome", "passed"))

        # Normalize status
        status_map = {
            "pass": "passed",
            "passed": "passed",
            "fail": "failed",
            "failed": "failed",
            "skip": "skipped",
            "skipped": "skipped",
            "error": "error",
        }
        status = status_map.get(status.lower(), "passed")

        duration = float(test.get("duration", 0))
        message = test.get("message", test.get("longrepr"))
        if message:
            message = str(message)[:200]

        validates = self._extract_req_ids(f"{classname} {name}", source_path)

        # Generate canonical TEST node ID using test_identity utility
        test_id = build_test_id_from_result(classname, name)

        return {
            "id": f"{source_path}:{classname}::{name}",
            "name": name,
            "classname": classname,
            "status": status,
            "duration": duration,
            "message": message,
            "validates": validates,
            "source_path": source_path,
            "test_id": test_id,
        }

    def claim_and_parse(
        self,
        lines: list[tuple[int, str]],
        context: ParseContext,
    ) -> Iterator[ParsedContent]:
        """Claim and parse pytest JSON content via the standard pipeline.

        Reassembles lines into full JSON content, delegates to ``parse()``,
        and yields ``ParsedContent`` for each test result.

        Args:
            lines: List of (line_number, content) tuples.
            context: Parsing context with file info.

        Yields:
            ParsedContent for each test result found.
        """
        content = "\n".join(text for _, text in lines)
        results = self.parse(content, context.file_path)
        for result in results:
            yield ParsedContent(
                content_type="test_result",
                start_line=lines[0][0] if lines else 1,
                end_line=lines[-1][0] if lines else 1,
                raw_text="",
                parsed_data=result,
            )

    def _extract_req_ids(self, text: str, source_file: str | None = None) -> list[str]:
        """Extract requirement IDs from text.

        Args:
            text: Text to search for requirement IDs.
            source_file: Optional source file for file-specific config.

        Returns:
            List of normalized requirement IDs (using hyphens).
        """
        pattern_config = self._get_pattern_config()
        ref_config = self._get_reference_config(source_file)

        # Use shared extraction function
        ids = extract_ids_from_text(text, pattern_config, ref_config)

        # Normalize: replace underscores with hyphens
        normalized = []
        for req_id in ids:
            normalized_id = req_id.replace("_", "-")
            if normalized_id not in normalized:
                normalized.append(normalized_id)

        return normalized

    def can_parse(self, file_path: Path) -> bool:
        """Check if this parser can handle the given file.

        Args:
            file_path: Path to the file.

        Returns:
            True for JSON files that look like pytest results.
        """
        name = file_path.name.lower()
        return file_path.suffix.lower() == ".json" and (
            "pytest" in name or "test" in name or "result" in name
        )


def create_parser(
    pattern_config: PatternConfig | None = None,
    reference_resolver: ReferenceResolver | None = None,
    base_path: Path | None = None,
) -> PytestJSONParser:
    """Factory function to create a PytestJSONParser.

    Args:
        pattern_config: Optional configuration for ID structure.
        reference_resolver: Optional resolver for file-specific configs.
        base_path: Optional base path for resolving file paths.

    Returns:
        New PytestJSONParser instance.
    """
    return PytestJSONParser(pattern_config, reference_resolver, base_path)
