"""Tests for worker normalize security and validation.

Tests path traversal prevention, size limits, and input validation
in the normalize pipeline.
"""

from __future__ import annotations

import json

import pytest

try:
    import zstandard as zstd
    HAS_ZSTD = True
except ImportError:
    HAS_ZSTD = False

from worker.normalize import (
    MAX_COMPRESSED_FILE_SIZE,
    MAX_DECOMPRESSED_SIZE,
    MAX_RAW_FILE_SIZE,
    _validate_filename,
    normalize_bundle,
)


class TestValidateFilename:
    """Tests for _validate_filename() path traversal prevention."""

    def test_valid_simple_name(self):
        _validate_filename("events.jsonl.zst")

    def test_valid_with_dashes_underscores(self):
        _validate_filename("deps_http-2025.jsonl.zst")

    def test_rejects_empty(self):
        with pytest.raises(ValueError):
            _validate_filename("")

    def test_rejects_path_traversal_dotdot(self):
        with pytest.raises(ValueError):
            _validate_filename("../etc/passwd")

    def test_rejects_path_traversal_slash(self):
        with pytest.raises(ValueError):
            _validate_filename("subdir/file.txt")

    def test_rejects_backslash(self):
        with pytest.raises(ValueError):
            _validate_filename("subdir\\file.txt")

    def test_rejects_absolute_path(self):
        with pytest.raises(ValueError):
            _validate_filename("/etc/passwd")

    def test_rejects_hidden_dotdot(self):
        with pytest.raises(ValueError):
            _validate_filename("..file")

    def test_rejects_windows_drive(self):
        with pytest.raises(ValueError):
            _validate_filename("C:\\Windows\\system32")


class TestNormalizeBundleSecurity:
    """Tests for normalize_bundle security checks."""

    def test_rejects_traversal_in_filename(self, tmp_path):
        bundle = tmp_path / "bundle"
        bundle.mkdir()
        out = tmp_path / "out"
        out.mkdir()

        with pytest.raises(ValueError):
            normalize_bundle(bundle, out, ["../../../etc/passwd"])

    def test_rejects_absolute_path_in_files(self, tmp_path):
        bundle = tmp_path / "bundle"
        bundle.mkdir()
        out = tmp_path / "out"
        out.mkdir()

        with pytest.raises(ValueError):
            normalize_bundle(bundle, out, ["/etc/passwd"])

    @pytest.mark.skipif(not HAS_ZSTD, reason="zstandard not installed")
    def test_normalizes_valid_jsonl_zst(self, tmp_path):
        """Valid compressed JSONL should be normalized successfully."""
        bundle = tmp_path / "bundle"
        bundle.mkdir()
        out = tmp_path / "out"
        out.mkdir()

        # Create a valid .jsonl.zst file
        records = [{"type": "HTTP", "seq": i} for i in range(5)]
        jsonl = "\n".join(json.dumps(r) for r in records)
        cctx = zstd.ZstdCompressor()
        compressed = cctx.compress(jsonl.encode())
        (bundle / "deps_http.jsonl.zst").write_bytes(compressed)

        normalize_bundle(bundle, out, ["deps_http.jsonl.zst"])

        # Output file should exist (may have been re-compressed)
        out_files = list(out.rglob("deps_http*"))
        assert len(out_files) > 0

    def test_copies_raw_file_within_limits(self, tmp_path):
        """Raw files under size limit should be copied."""
        bundle = tmp_path / "bundle"
        bundle.mkdir()
        out = tmp_path / "out"
        out.mkdir()

        # Create a small raw file
        (bundle / "manifest.json").write_text('{"version": 1}')
        normalize_bundle(bundle, out, ["manifest.json"])

        assert (out / "manifest.json").exists()

    def test_destination_stays_within_outdir(self, tmp_path):
        """Output should not escape the output directory."""
        bundle = tmp_path / "bundle"
        bundle.mkdir()
        out = tmp_path / "out"
        out.mkdir()

        # Create valid file
        (bundle / "test.json").write_text("{}")
        normalize_bundle(bundle, out, ["test.json"])

        # Verify output is within out dir
        for f in out.rglob("*"):
            assert str(f.resolve()).startswith(str(out.resolve()))


class TestNormalizeSizeLimits:
    """Tests for size limit enforcement."""

    def test_constants_are_reasonable(self):
        """Size limits should be reasonable values."""
        assert MAX_COMPRESSED_FILE_SIZE == 100 * 1024 * 1024  # 100 MB
        assert MAX_DECOMPRESSED_SIZE == 500 * 1024 * 1024  # 500 MB
        assert MAX_RAW_FILE_SIZE == 100 * 1024 * 1024  # 100 MB

    @pytest.mark.skipif(not HAS_ZSTD, reason="zstandard not installed")
    def test_rejects_invalid_json_in_jsonl(self, tmp_path):
        """Invalid JSON lines should be rejected."""
        bundle = tmp_path / "bundle"
        bundle.mkdir()
        out = tmp_path / "out"
        out.mkdir()

        # Create a .jsonl.zst with invalid JSON
        bad_jsonl = "this is not json\n{also not json}"
        cctx = zstd.ZstdCompressor()
        compressed = cctx.compress(bad_jsonl.encode())
        (bundle / "bad.jsonl.zst").write_bytes(compressed)

        with pytest.raises((ValueError, json.JSONDecodeError)):
            normalize_bundle(bundle, out, ["bad.jsonl.zst"])

    def test_handles_empty_file_list(self, tmp_path):
        """Empty file list should not crash."""
        bundle = tmp_path / "bundle"
        bundle.mkdir()
        out = tmp_path / "out"
        out.mkdir()

        result = normalize_bundle(bundle, out, [])
        assert result == out

    def test_handles_missing_file(self, tmp_path):
        """Missing files in bundle should raise."""
        bundle = tmp_path / "bundle"
        bundle.mkdir()
        out = tmp_path / "out"
        out.mkdir()

        with pytest.raises((FileNotFoundError, ValueError)):
            normalize_bundle(bundle, out, ["nonexistent.jsonl.zst"])
