"""Parity regression tests for native SDD CLI.

Validates that native CLI outputs match expected golden fixture schemas,
ensuring backward compatibility and output structure stability.

Tests cover:
- Response envelope consistency (success/data/error/meta structure)
- Key field presence for each command type
- Error response structure consistency
- Schema stability against golden fixtures
"""

import json
from pathlib import Path

import pytest
from click.testing import CliRunner

from foundry_mcp.cli.main import cli


# Golden fixtures directory
FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "golden"


def load_golden_fixture(name: str) -> dict:
    """Load a golden fixture by name."""
    fixture_path = FIXTURES_DIR / f"{name}.json"
    if not fixture_path.exists():
        pytest.skip(f"Golden fixture not found: {name}")
    with open(fixture_path) as f:
        return json.load(f)


def validate_envelope_structure(response: dict, is_error: bool = False) -> list[str]:
    """Validate response envelope structure and return any violations.

    Per dev_docs/codebase_standards/mcp_response_schema.md, the envelope is:
    - success (bool): required
    - data (object): required - contains payload on success, error context on failure
    - error (string|null): required - null on success, message string on failure
    - meta (object): required - contains version and optional metadata

    Args:
        response: The response dict to validate
        is_error: If True, validates as error response
    """
    violations = []

    # All responses require success, data, and error keys per response-v2 contract
    required_keys = {"success", "data", "error"}
    missing_keys = required_keys - set(response.keys())
    if missing_keys:
        violations.append(f"Missing envelope keys: {missing_keys}")

    # Type checks
    if "success" in response and not isinstance(response["success"], bool):
        violations.append(
            f"'success' must be boolean, got {type(response['success']).__name__}"
        )

    if "data" in response and not isinstance(response["data"], dict):
        violations.append(
            f"'data' must be object, got {type(response['data']).__name__}"
        )

    if (
        "error" in response
        and response["error"] is not None
        and not isinstance(response["error"], str)
    ):
        violations.append(
            f"'error' must be string or null, got {type(response['error']).__name__}"
        )

    # Consistency checks
    if response.get("success") is True and response.get("error") is not None:
        violations.append("Successful response should have error=null")

    if response.get("success") is False and response.get("error") is None:
        violations.append("Failed response should have error message")

    # Error responses should have structured error context in data (per mcp_response_schema.md)
    # data should contain error_code, error_type, remediation when available

    return violations


def compare_schema_keys(expected: dict, actual: dict, path: str = "") -> list[str]:
    """Compare schema keys recursively, returning differences."""
    differences = []

    # Skip fixture metadata fields
    skip_keys = {"fixture_version"}
    expected_keys = set(expected.keys()) - skip_keys
    actual_keys = set(actual.keys())

    # Check for missing required keys (present in expected but not actual)
    missing = expected_keys - actual_keys
    if missing:
        differences.append(f"{path}: missing keys {missing}")

    # Recurse into nested dicts for matching keys
    common_keys = expected_keys & actual_keys
    for key in common_keys:
        if isinstance(expected[key], dict) and isinstance(actual[key], dict):
            nested_path = f"{path}.{key}" if path else key
            differences.extend(
                compare_schema_keys(expected[key], actual[key], nested_path)
            )

    return differences


@pytest.fixture
def cli_runner():
    """Create a Click CLI runner."""
    return CliRunner()


@pytest.fixture
def temp_specs_dir(tmp_path):
    """Create a temporary specs directory with test fixtures."""
    specs_dir = tmp_path / "specs"
    active_dir = specs_dir / "active"
    active_dir.mkdir(parents=True)

    # Create a test spec that matches golden fixture expectations
    test_spec = {
        "id": "example-spec",
        "title": "Example Specification",
        "version": "1.0.0",
        "status": "active",
        "hierarchy": {
            "spec-root": {
                "type": "root",
                "title": "Example Specification",
                "children": ["phase-1"],
                "status": "in_progress",
            },
            "phase-1": {
                "type": "phase",
                "title": "Phase 1",
                "parent": "spec-root",
                "children": ["task-1-1", "task-1-2"],
                "status": "in_progress",
            },
            "task-1-1": {
                "type": "task",
                "title": "First task",
                "parent": "phase-1",
                "status": "completed",
                "metadata": {"description": "First task implementation"},
                "dependencies": {},
            },
            "task-1-2": {
                "type": "task",
                "title": "Second task",
                "parent": "phase-1",
                "status": "pending",
                "metadata": {"description": "Second task"},
                "dependencies": {},
            },
        },
        "journal": [],
    }

    spec_file = active_dir / "example-spec.json"
    spec_file.write_text(json.dumps(test_spec, indent=2))

    return specs_dir


class TestResponseEnvelopeParity:
    """Tests that all CLI responses follow the standard envelope structure."""

    def test_success_response_envelope(self, cli_runner, temp_specs_dir):
        """Success responses follow envelope structure."""
        result = cli_runner.invoke(
            cli, ["--specs-dir", str(temp_specs_dir), "dev", "check"]
        )
        assert result.exit_code == 0

        response = json.loads(result.output)
        violations = validate_envelope_structure(response)
        assert not violations, f"Envelope violations: {violations}"

        # Additional success checks
        assert response["success"] is True
        assert response["data"] is not None
        assert response["error"] is None

    def test_error_response_envelope(self, cli_runner, temp_specs_dir):
        """Error responses follow envelope structure."""
        result = cli_runner.invoke(
            cli,
            [
                "--specs-dir",
                str(temp_specs_dir),
                "validate",
                "check",
                "nonexistent-spec",
            ],
        )
        # May succeed or fail depending on implementation
        response = json.loads(result.output)
        violations = validate_envelope_structure(response)
        assert not violations, f"Envelope violations: {violations}"

        if not response["success"]:
            # Per response-v2 contract: error is a string message
            assert response["error"] is not None
            assert isinstance(response["error"], str)
            # Structured error context is in data (error_code, error_type, etc.)
            assert isinstance(response["data"], dict)


class TestGoldenFixtureParity:
    """Tests that CLI outputs match golden fixture schemas."""

    def test_dev_check_matches_golden(self, cli_runner, temp_specs_dir):
        """dev check output matches expected schema."""
        result = cli_runner.invoke(
            cli, ["--specs-dir", str(temp_specs_dir), "dev", "check"]
        )
        assert result.exit_code == 0

        actual = json.loads(result.output)

        # Validate envelope structure
        violations = validate_envelope_structure(actual)
        assert not violations, f"Envelope violations: {violations}"

        # Verify key structure matches
        assert "tools" in actual["data"]
        assert "all_required_available" in actual["data"]

    def test_validation_success_matches_golden(self, cli_runner, temp_specs_dir):
        """validate check output matches expected schema."""
        result = cli_runner.invoke(
            cli,
            ["--specs-dir", str(temp_specs_dir), "validate", "check", "example-spec"],
        )

        actual = json.loads(result.output)
        load_golden_fixture("success_validation")

        # Validate envelope structure
        violations = validate_envelope_structure(actual)
        assert not violations, f"Envelope violations: {violations}"

        if actual["success"]:
            # Verify key fields present
            required_fields = {"spec_id", "is_valid"}
            actual_fields = set(actual["data"].keys())
            missing = required_fields - actual_fields
            assert not missing, f"Missing validation fields: {missing}"

    def test_error_not_found_matches_golden(self, cli_runner, temp_specs_dir):
        """Not found error matches expected schema."""
        result = cli_runner.invoke(
            cli,
            [
                "--specs-dir",
                str(temp_specs_dir),
                "validate",
                "check",
                "nonexistent-spec-id",
            ],
        )

        actual = json.loads(result.output)
        load_golden_fixture("error_not_found")

        # Validate envelope structure
        violations = validate_envelope_structure(actual)
        assert not violations, f"Envelope violations: {violations}"

        # Error responses should have consistent structure per response-v2
        if not actual["success"]:
            # error is a string message
            assert actual["error"] is not None
            assert isinstance(actual["error"], str)
            # Structured error context (error_code, error_type) is in data
            assert isinstance(actual["data"], dict)


class TestCommandGroupParity:
    """Tests that all command groups produce consistent output."""

    def test_specs_analyze_produces_json(self, cli_runner, temp_specs_dir):
        """specs analyze produces valid JSON output."""
        result = cli_runner.invoke(
            cli, ["--specs-dir", str(temp_specs_dir), "specs", "analyze"]
        )
        assert result.exit_code == 0

        response = json.loads(result.output)
        violations = validate_envelope_structure(response)
        assert not violations, f"Envelope violations: {violations}"

    def test_session_status_produces_json(self, cli_runner, temp_specs_dir):
        """session status produces valid JSON output."""
        result = cli_runner.invoke(
            cli, ["--specs-dir", str(temp_specs_dir), "session", "status"]
        )
        assert result.exit_code == 0

        response = json.loads(result.output)
        violations = validate_envelope_structure(response)
        assert not violations, f"Envelope violations: {violations}"

    def test_session_capabilities_produces_json(self, cli_runner, temp_specs_dir):
        """session capabilities produces valid JSON output."""
        result = cli_runner.invoke(
            cli, ["--specs-dir", str(temp_specs_dir), "session", "capabilities"]
        )
        assert result.exit_code == 0

        response = json.loads(result.output)
        violations = validate_envelope_structure(response)
        assert not violations, f"Envelope violations: {violations}"

        # Should have capabilities info
        assert response["success"] is True
        assert "capabilities" in response["data"]

    def test_lifecycle_state_produces_json(self, cli_runner, temp_specs_dir):
        """lifecycle state produces valid JSON output."""
        result = cli_runner.invoke(
            cli,
            ["--specs-dir", str(temp_specs_dir), "lifecycle", "state", "example-spec"],
        )

        response = json.loads(result.output)
        violations = validate_envelope_structure(response)
        assert not violations, f"Envelope violations: {violations}"


class TestOutputStability:
    """Tests for output stability across invocations."""

    def test_repeated_invocations_consistent(self, cli_runner, temp_specs_dir):
        """Repeated command invocations produce consistent structure."""
        results = []
        for _ in range(3):
            result = cli_runner.invoke(
                cli, ["--specs-dir", str(temp_specs_dir), "dev", "check"]
            )
            assert result.exit_code == 0
            results.append(json.loads(result.output))

        # All responses should have same keys
        first_keys = set(results[0].keys())
        for i, response in enumerate(results[1:], 2):
            assert set(response.keys()) == first_keys, (
                f"Invocation {i} has different keys"
            )

        # All should have same data keys
        first_data_keys = set(results[0]["data"].keys())
        for i, response in enumerate(results[1:], 2):
            assert set(response["data"].keys()) == first_data_keys

    def test_no_ansi_in_output(self, cli_runner, temp_specs_dir):
        """CLI output contains no ANSI escape codes."""
        result = cli_runner.invoke(
            cli, ["--specs-dir", str(temp_specs_dir), "dev", "check"]
        )
        assert result.exit_code == 0

        # Check for common ANSI escape sequences
        ansi_patterns = ["\x1b[", "\033[", "\x1b]"]
        for pattern in ansi_patterns:
            assert pattern not in result.output, (
                f"Found ANSI escape in output: {pattern!r}"
            )

    def test_output_is_valid_json(self, cli_runner, temp_specs_dir):
        """All output is valid JSON (no extra text)."""
        commands = [
            ["dev", "check"],
            ["specs", "analyze"],
            ["session", "status"],
        ]

        for cmd in commands:
            result = cli_runner.invoke(cli, ["--specs-dir", str(temp_specs_dir)] + cmd)

            # Output should be parseable as JSON
            try:
                data = json.loads(result.output)
            except json.JSONDecodeError as e:
                pytest.fail(
                    f"Command {cmd} produced invalid JSON: {e}\nOutput: {result.output[:200]}"
                )

            # Should be a dict (not list or primitive)
            assert isinstance(data, dict), (
                f"Command {cmd} should return object, got {type(data)}"
            )
