"""Tests for individual worker pipeline steps.

Tests slice generation, harness building, and Docker validation
functions in isolation.
"""

from __future__ import annotations

import json
import os
from pathlib import Path
from unittest.mock import patch

import pytest

from worker.harness import (
    _generate_dockerfile,
    _generate_requirements,
    _is_path_safe,
)
from worker.slice import _load_records, generate_slice
from worker.validate import (
    _hash_signature,
    _parse_int_env,
    _sanitize_docker_tag,
    compute_report,
)

# --- Docker Tag Sanitization ---


class TestSanitizeDockerTag:
    """Tests for _sanitize_docker_tag()."""

    def test_valid_simple_name(self):
        tag = _sanitize_docker_tag("my-test")
        assert tag.startswith("autodebug-repro-")
        assert "my-test" in tag

    def test_lowercases_input(self):
        tag = _sanitize_docker_tag("MyTest")
        assert tag == tag.lower()

    def test_replaces_invalid_chars(self):
        tag = _sanitize_docker_tag("my test@v1")
        assert " " not in tag
        assert "@" not in tag

    def test_removes_leading_non_alnum(self):
        tag = _sanitize_docker_tag("---test")
        # After prefix, the name part should start with alnum
        name_part = tag.replace("autodebug-repro-", "")
        if name_part:
            assert name_part[0].isalnum() or name_part[0] in ".-_"

    def test_limits_length(self):
        long_name = "a" * 200
        tag = _sanitize_docker_tag(long_name)
        assert len(tag) <= 128

    def test_empty_raises(self):
        with pytest.raises(ValueError):
            _sanitize_docker_tag("")

    def test_custom_prefix(self):
        tag = _sanitize_docker_tag("test", prefix="custom-")
        assert tag.startswith("custom-")


# --- Int Environment Variable Parsing ---


class TestParseIntEnv:
    """Tests for _parse_int_env()."""

    def test_returns_default_when_unset(self):
        with patch.dict(os.environ, {}, clear=False):
            os.environ.pop("TEST_PARSE_INT", None)
            result = _parse_int_env("TEST_PARSE_INT", default=42)
            assert result == 42

    def test_parses_valid_int(self):
        with patch.dict(os.environ, {"TEST_PARSE_INT": "100"}):
            result = _parse_int_env("TEST_PARSE_INT", default=42)
            assert result == 100

    def test_caps_at_max_value(self):
        with patch.dict(os.environ, {"TEST_PARSE_INT": "9999"}):
            result = _parse_int_env("TEST_PARSE_INT", default=42, max_value=100)
            assert result == 100

    def test_invalid_returns_default(self):
        with patch.dict(os.environ, {"TEST_PARSE_INT": "not_a_number"}):
            result = _parse_int_env("TEST_PARSE_INT", default=42)
            assert result == 42


# --- Hash Signature ---


class TestHashSignature:
    """Tests for _hash_signature() determinism."""

    def test_deterministic(self):
        stack = [{"file": "test.py", "line": 10, "func": "test_foo"}]
        h1 = _hash_signature("ValueError", "bad value", stack)
        h2 = _hash_signature("ValueError", "bad value", stack)
        assert h1 == h2

    def test_different_types_different_hash(self):
        stack = [{"file": "test.py", "line": 10, "func": "test_foo"}]
        h1 = _hash_signature("ValueError", "bad value", stack)
        h2 = _hash_signature("KeyError", "bad value", stack)
        assert h1 != h2

    def test_different_stack_different_hash(self):
        stack1 = [{"file": "test.py", "line": 10, "func": "test_foo"}]
        stack2 = [{"file": "test.py", "line": 20, "func": "test_bar"}]
        h1 = _hash_signature("ValueError", "bad", stack1)
        h2 = _hash_signature("ValueError", "bad", stack2)
        assert h1 != h2


# --- Compute Report ---


class TestComputeReport:
    """Tests for compute_report() validation results."""

    def test_all_matching_signatures(self):
        results = [
            {"exit_code": 1, "signature": "abc123"},
            {"exit_code": 1, "signature": "abc123"},
            {"exit_code": 1, "signature": "abc123"},
        ]
        report = compute_report(results)
        assert report["determinism_score"] == 1.0
        assert report["stable"] is True

    def test_divergent_signatures(self):
        results = [
            {"exit_code": 1, "signature": "abc123"},
            {"exit_code": 1, "signature": "def456"},
            {"exit_code": 1, "signature": "abc123"},
        ]
        report = compute_report(results)
        # With 2 out of 3 matching, determinism < 1.0 or exactly 1.0 if majority-based
        assert report["determinism_score"] <= 1.0

    def test_empty_results(self):
        report = compute_report([])
        assert report["status"] == "FAILED_VALIDATION"

    def test_none_signatures(self):
        results = [
            {"exit_code": None, "signature": None},
        ]
        report = compute_report(results)
        # None signatures indicate validation issues
        assert "status" in report

    def test_with_blocking_unsupported(self):
        results = [{"exit_code": 1, "signature": "abc"}]
        manifest = {"has_blocking_unsupported": True}
        report = compute_report(results, manifest=manifest)
        assert report["status"] == "FAILED_UNSUPPORTED"


# --- Slice Generation ---


class TestLoadRecords:
    """Tests for _load_records()."""

    def test_loads_valid_jsonl(self, tmp_path):
        f = tmp_path / "records.jsonl"
        records = [{"type": "HTTP", "seq": i} for i in range(3)]
        f.write_text("\n".join(json.dumps(r) for r in records))

        result = _load_records(f)
        assert len(result) == 3

    def test_returns_empty_for_missing_file(self, tmp_path):
        result = _load_records(tmp_path / "nonexistent.jsonl")
        assert result == []

    def test_skips_empty_lines(self, tmp_path):
        f = tmp_path / "gaps.jsonl"
        f.write_text('{"seq": 1}\n\n{"seq": 2}\n')

        result = _load_records(f)
        assert len(result) == 2


class TestGenerateSlice:
    """Tests for generate_slice()."""

    def test_generates_markdown_output(self, tmp_path):
        transcripts = tmp_path / "transcripts"
        transcripts.mkdir()

        # Create minimal events file with correct field name
        events = [
            {"type": "START", "seq": 1},
            {"type": "EXCEPTION", "seq": 10, "exc_type": "ValueError",
             "message": "bad value", "stack_trace": [
                 {"file": "test.py", "line": 10, "func": "test_foo"},
             ]},
        ]
        (transcripts / "events.jsonl").write_text(
            "\n".join(json.dumps(r) for r in events)
        )

        out = tmp_path / "slice.md"
        generate_slice(transcripts, out)
        assert out.exists()
        content = out.read_text()
        assert "ValueError" in content or "Causal Slice" in content

    def test_handles_no_exception(self, tmp_path):
        transcripts = tmp_path / "transcripts"
        transcripts.mkdir()

        events = [{"type": "START", "seq": 1}]
        (transcripts / "events.jsonl").write_text(json.dumps(events[0]))

        out = tmp_path / "slice.md"
        generate_slice(transcripts, out)
        assert out.exists()

    def test_handles_empty_directory(self, tmp_path):
        transcripts = tmp_path / "transcripts"
        transcripts.mkdir()

        out = tmp_path / "slice.md"
        generate_slice(transcripts, out)
        assert out.exists()


# --- Dockerfile Generation ---


class TestGenerateDockerfile:
    """Tests for _generate_dockerfile()."""

    def test_tier_a_default(self):
        df = _generate_dockerfile()
        assert "python:" in df.lower()
        assert "pip install" in df.lower() or "requirements" in df.lower()

    def test_tier_b_poetry(self):
        df = _generate_dockerfile(tier="B_POETRY")
        assert "poetry" in df.lower()

    def test_non_root_user(self):
        df = _generate_dockerfile()
        assert "repro" in df.lower() or "user" in df.lower()

    def test_python_version_customizable(self):
        df = _generate_dockerfile(python_version="3.12")
        assert "3.12" in df


# --- Requirements Generation ---


class TestGenerateRequirements:
    """Tests for _generate_requirements()."""

    def test_includes_core_packages(self):
        packages = {"pytest": "7.4.0", "requests": "2.31.0", "numpy": "1.24.0"}
        result = _generate_requirements(packages)
        assert "pytest" in result
        assert "requests" in result

    def test_sorted_output(self):
        packages = {"zstandard": "0.21.0", "aiohttp": "3.8.0", "pytest": "7.4.0"}
        result = _generate_requirements(packages)
        lines = [l for l in result.strip().split("\n") if l and not l.startswith("#")]
        names = [l.split("==")[0].lower() for l in lines]
        assert names == sorted(names)


# --- Path Safety ---


class TestIsPathSafe:
    """Tests for _is_path_safe()."""

    def test_safe_path(self, tmp_path):
        safe = tmp_path / "myproject" / "test.py"
        safe.parent.mkdir(parents=True, exist_ok=True)
        safe.write_text("pass")
        assert _is_path_safe(safe) is True

    def test_dangerous_paths_exist_as_constant(self):
        """_DANGEROUS_PATHS constant should include system paths."""
        from worker.harness import _DANGEROUS_PATHS
        assert "/etc" in _DANGEROUS_PATHS or any("/etc" in p for p in _DANGEROUS_PATHS)

    def test_rejects_dotdot(self):
        assert _is_path_safe(Path("../../secret")) is False
