"""Tests for filesystem scanner."""

from pathlib import Path

import pytest

from certisigma_census.scanner import scan_directory, walk_files


# ─── Hidden file handling ──────────────────────────────────────────────────

def test_walk_files_skips_hidden(tmp_path: Path):
    (tmp_path / "visible.txt").write_text("hello")
    (tmp_path / ".hidden").write_text("secret")
    (tmp_path / ".git").mkdir()
    (tmp_path / ".git" / "config").write_text("x")

    files = list(walk_files(tmp_path, skip_hidden=True))
    names = [f.name for f in files]
    assert "visible.txt" in names
    assert ".hidden" not in names
    assert "config" not in names


def test_walk_files_includes_hidden(tmp_path: Path):
    (tmp_path / "visible.txt").write_text("hello")
    (tmp_path / ".dotfile").write_text("dot")

    files = list(walk_files(tmp_path, skip_hidden=False, skip_dirs=frozenset()))
    names = [f.name for f in files]
    assert "visible.txt" in names
    assert ".dotfile" in names


# ─── Size filters ──────────────────────────────────────────────────────────

def test_scan_directory_produces_manifest(tmp_path: Path):
    for i in range(3):
        (tmp_path / f"file_{i}.txt").write_text(f"content {i}")

    manifest = scan_directory(tmp_path)
    assert manifest.total == 3
    assert manifest.root_dir == str(tmp_path)
    for entry in manifest.entries.values():
        assert len(entry.hash_hex) == 64
        assert entry.size > 0


def test_scan_directory_skips_oversized(tmp_path: Path):
    (tmp_path / "small.txt").write_text("ok")
    big = tmp_path / "big.bin"
    big.write_bytes(b"x" * 200)

    manifest = scan_directory(tmp_path, max_file_size=100)
    assert manifest.total == 1
    assert list(manifest.entries.values())[0].path == "small.txt"


def test_walk_files_min_size(tmp_path: Path):
    (tmp_path / "tiny.txt").write_bytes(b"x")
    (tmp_path / "medium.txt").write_bytes(b"x" * 500)
    (tmp_path / "large.dat").write_bytes(b"x" * 2000)

    files = list(walk_files(tmp_path, min_file_size=100))
    names = [f.name for f in files]
    assert "tiny.txt" not in names
    assert "medium.txt" in names
    assert "large.dat" in names


# ─── Include / exclude globs ──────────────────────────────────────────────

def test_walk_files_include_glob(tmp_path: Path):
    (tmp_path / "report.pdf").write_bytes(b"pdf")
    (tmp_path / "data.csv").write_bytes(b"csv")
    (tmp_path / "notes.txt").write_bytes(b"txt")
    (tmp_path / "image.png").write_bytes(b"png")

    files = list(walk_files(tmp_path, include_globs=["*.pdf", "*.csv"]))
    names = sorted(f.name for f in files)
    assert names == ["data.csv", "report.pdf"]


def test_walk_files_exclude_glob(tmp_path: Path):
    (tmp_path / "report.pdf").write_bytes(b"pdf")
    (tmp_path / "debug.log").write_bytes(b"log")
    (tmp_path / "access.log").write_bytes(b"log")
    (tmp_path / "data.csv").write_bytes(b"csv")

    files = list(walk_files(tmp_path, exclude_globs=["*.log"]))
    names = sorted(f.name for f in files)
    assert names == ["data.csv", "report.pdf"]


def test_walk_files_include_and_exclude_combined(tmp_path: Path):
    """Include narrows, then exclude removes from that set."""
    (tmp_path / "report.pdf").write_bytes(b"pdf")
    (tmp_path / "draft-report.pdf").write_bytes(b"pdf")
    (tmp_path / "data.csv").write_bytes(b"csv")
    (tmp_path / "notes.txt").write_bytes(b"txt")

    files = list(walk_files(
        tmp_path,
        include_globs=["*.pdf"],
        exclude_globs=["draft-*"],
    ))
    names = [f.name for f in files]
    assert names == ["report.pdf"]


def test_walk_files_combined_filters(tmp_path: Path):
    """All filter dimensions work together: include + exclude + size."""
    (tmp_path / "small.pdf").write_bytes(b"x")
    (tmp_path / "good.pdf").write_bytes(b"x" * 500)
    (tmp_path / "big.pdf").write_bytes(b"x" * 5000)
    (tmp_path / "good.txt").write_bytes(b"x" * 500)
    (tmp_path / "temp.pdf").write_bytes(b"x" * 500)

    files = list(walk_files(
        tmp_path,
        include_globs=["*.pdf"],
        exclude_globs=["temp*"],
        min_file_size=10,
        max_file_size=2000,
    ))
    names = [f.name for f in files]
    assert names == ["good.pdf"]


def test_scan_directory_with_filters(tmp_path: Path):
    (tmp_path / "report.pdf").write_bytes(b"pdf content here")
    (tmp_path / "debug.log").write_bytes(b"log content here")
    (tmp_path / "data.csv").write_bytes(b"csv content here")

    manifest = scan_directory(
        tmp_path,
        include_globs=["*.pdf", "*.csv"],
        exclude_globs=["*.log"],
    )
    assert manifest.total == 2
    paths = sorted(e.path for e in manifest.entries.values())
    assert paths == ["data.csv", "report.pdf"]


# ─── Resume mode ──────────────────────────────────────────────────────────

def test_scan_resume_skips_unchanged(tmp_path: Path):
    """Files already in manifest with same size/mtime are not re-hashed."""
    (tmp_path / "existing.txt").write_bytes(b"already scanned")
    (tmp_path / "new.txt").write_bytes(b"fresh file")

    first = scan_directory(tmp_path)
    assert first.total == 2

    second = scan_directory(tmp_path, existing_manifest=first)
    assert second.total == 2
    for entry in second.entries.values():
        assert len(entry.hash_hex) == 64


def test_scan_resume_rehashes_modified(tmp_path: Path):
    """Changed mtime/size triggers re-hash even if file was in manifest."""
    f = tmp_path / "changing.txt"
    f.write_bytes(b"version 1")

    first = scan_directory(tmp_path)
    old_hash = list(first.entries.keys())[0]

    import time
    time.sleep(0.05)
    f.write_bytes(b"version 2 with more content")

    second = scan_directory(tmp_path, existing_manifest=first)
    new_hash = list(second.entries.keys())[0]
    assert new_hash != old_hash


def test_scan_resume_picks_up_new_files(tmp_path: Path):
    """New files added after first scan are hashed on resume."""
    (tmp_path / "original.txt").write_bytes(b"original")
    first = scan_directory(tmp_path)
    assert first.total == 1

    (tmp_path / "added.txt").write_bytes(b"added later")
    second = scan_directory(tmp_path, existing_manifest=first)
    assert second.total == 2


# ─── Parallel hashing ─────────────────────────────────────────────────────

def test_scan_parallel_produces_same_manifest(tmp_path: Path):
    """Parallel scan (workers=2) yields the same hashes as sequential."""
    for i in range(5):
        (tmp_path / f"file_{i}.txt").write_text(f"content {i}")

    serial = scan_directory(tmp_path, workers=1)
    parallel = scan_directory(tmp_path, workers=2)

    assert serial.total == parallel.total
    assert set(serial.entries.keys()) == set(parallel.entries.keys())


def test_scan_parallel_single_file(tmp_path: Path):
    """Workers > 1 with a single file degrades to sequential safely."""
    (tmp_path / "only.txt").write_text("alone")
    manifest = scan_directory(tmp_path, workers=4)
    assert manifest.total == 1


def test_scan_parallel_resume_mode(tmp_path: Path):
    """Parallel scan with resume skips unchanged files."""
    (tmp_path / "a.txt").write_bytes(b"aaa")
    (tmp_path / "b.txt").write_bytes(b"bbb")

    first = scan_directory(tmp_path, workers=2)
    assert first.total == 2

    (tmp_path / "c.txt").write_bytes(b"ccc")
    second = scan_directory(tmp_path, workers=2, existing_manifest=first)
    assert second.total == 3
