import gzip
import os
import struct
from unittest.mock import patch

from vcm_file_uploader._types import CompressionInfo
from vcm_file_uploader.compress import (
    DEFAULT_COMPRESS_THRESHOLD,
    compress_file,
    is_ascii_file,
    maybe_compress,
)


class TestIsAsciiFile:
    def test_plain_text(self, tmp_path):
        p = tmp_path / "readme.txt"
        p.write_text("Hello, world!\nThis is a plain text file.\n")
        assert is_ascii_file(str(p)) is True

    def test_log_output(self, tmp_path):
        p = tmp_path / "amber.mdout"
        p.write_text("Step 1: energy = -1234.56\nStep 2: energy = -1234.78\n" * 100)
        assert is_ascii_file(str(p)) is True

    def test_csv_data(self, tmp_path):
        p = tmp_path / "results.csv"
        p.write_text("time,energy,temp\n1.0,-500.2,300.1\n2.0,-501.3,299.8\n" * 50)
        assert is_ascii_file(str(p)) is True

    def test_binary_with_nulls(self, tmp_path):
        p = tmp_path / "data.nc"
        p.write_bytes(b"\x00\x01\x02\x03" * 100)
        assert is_ascii_file(str(p)) is False

    def test_binary_netcdf_header(self, tmp_path):
        """NetCDF-like binary file with magic bytes."""
        p = tmp_path / "traj.nc"
        content = b"CDF\x01" + b"\x00" * 200 + struct.pack(">i", 42)
        p.write_bytes(content)
        assert is_ascii_file(str(p)) is False

    def test_binary_pickle(self, tmp_path):
        p = tmp_path / "checkpoint.pkl"
        p.write_bytes(b"\x80\x05\x95\x00\x00\x00\x00" + bytes(range(256)))
        assert is_ascii_file(str(p)) is False

    def test_empty_file(self, tmp_path):
        p = tmp_path / "empty.log"
        p.write_bytes(b"")
        assert is_ascii_file(str(p)) is False

    def test_missing_file(self, tmp_path):
        assert is_ascii_file(str(tmp_path / "missing.txt")) is False

    def test_tabs_and_carriage_returns(self, tmp_path):
        p = tmp_path / "tsv.txt"
        p.write_text("col1\tcol2\tcol3\r\nval1\tval2\tval3\r\n" * 100)
        assert is_ascii_file(str(p)) is True

    def test_mostly_binary_with_some_text(self, tmp_path):
        """File with >5% non-text bytes should be classified as binary."""
        p = tmp_path / "mixed.dat"
        text = b"normal text " * 80  # 960 bytes of text
        binary = bytes(range(128, 200)) * 2  # 144 bytes of high bytes (~13%)
        p.write_bytes(text + binary)
        assert is_ascii_file(str(p)) is False


class TestCompressFile:
    def test_compresses_file(self, tmp_path):
        src = tmp_path / "test.log"
        src.write_text("hello world\n" * 1000)
        original_size = src.stat().st_size

        compressed_path, info = compress_file(str(src))

        assert compressed_path == str(src) + ".gz"
        assert os.path.exists(compressed_path)
        assert info.algorithm == "gzip"
        assert info.level == 6
        assert info.original_bytes == original_size
        assert info.compressed_bytes < original_size
        assert isinstance(info, CompressionInfo)

        # Verify decompressible
        with gzip.open(compressed_path, "rt") as f:
            content = f.read()
        assert content == "hello world\n" * 1000

    def test_custom_output_path(self, tmp_path):
        src = tmp_path / "test.log"
        src.write_text("data " * 500)
        out = tmp_path / "custom.gz"

        compressed_path, info = compress_file(str(src), output_path=str(out))

        assert compressed_path == str(out)
        assert os.path.exists(str(out))

    def test_custom_level(self, tmp_path):
        src = tmp_path / "test.log"
        src.write_text("data " * 500)

        _, info = compress_file(str(src), level=9)

        assert info.level == 9


class TestMaybeCompress:
    def _make_large_ascii(self, tmp_path, name="output.dat", size=None):
        """Create an ASCII file above the default threshold."""
        if size is None:
            size = DEFAULT_COMPRESS_THRESHOLD + 1024
        p = tmp_path / name
        p.write_bytes(b"log line here\n" * (size // 14 + 1))
        return str(p)

    def _make_large_binary(self, tmp_path, name="data.nc", size=None):
        """Create a binary file above the default threshold."""
        if size is None:
            size = DEFAULT_COMPRESS_THRESHOLD + 1024
        p = tmp_path / name
        p.write_bytes(b"\x00\x01\x02\xff" * (size // 4 + 1))
        return str(p)

    def test_compresses_large_ascii_file(self, tmp_path):
        path = self._make_large_ascii(tmp_path)

        result = maybe_compress(path, check_stability=False)

        assert result is not None
        compressed_path, info = result
        assert compressed_path.endswith(".gz")
        assert info.original_bytes >= DEFAULT_COMPRESS_THRESHOLD

    def test_compresses_any_ascii_extension(self, tmp_path):
        """Any ASCII file compresses, regardless of extension."""
        for name in ["data.xyz", "results.csv", "notes.custom", "amber.mdout"]:
            path = self._make_large_ascii(tmp_path, name=name)
            result = maybe_compress(path, check_stability=False)
            assert result is not None, f"Failed to compress {name}"

    def test_skips_binary_file(self, tmp_path):
        path = self._make_large_binary(tmp_path)

        result = maybe_compress(path, check_stability=False)

        assert result is None

    def test_skips_below_threshold(self, tmp_path):
        p = tmp_path / "small.txt"
        p.write_text("small file")

        result = maybe_compress(str(p), check_stability=False)

        assert result is None

    def test_category_log_bypasses_content_check(self, tmp_path):
        """category='log' forces compression even for binary-looking files."""
        path = self._make_large_binary(tmp_path, name="weird.bin")

        result = maybe_compress(path, category="log", check_stability=False)

        assert result is not None

    def test_skips_missing_file(self, tmp_path):
        result = maybe_compress(str(tmp_path / "missing.log"), check_stability=False)

        assert result is None

    def test_skips_growing_file(self, tmp_path):
        path = self._make_large_ascii(tmp_path)

        with patch("vcm_file_uploader.compress.is_stable", return_value=False):
            result = maybe_compress(path, check_stability=True)

        assert result is None

    def test_compresses_stable_file(self, tmp_path):
        path = self._make_large_ascii(tmp_path)

        with patch("vcm_file_uploader.compress.is_stable", return_value=True):
            result = maybe_compress(path, check_stability=True)

        assert result is not None

    def test_custom_threshold(self, tmp_path):
        p = tmp_path / "small.txt"
        p.write_bytes(b"data\n" * 100)

        result = maybe_compress(str(p), threshold=10, check_stability=False)

        assert result is not None
