"""Tests for :mod:`mgf.vm.forensics.analysis.ioc` (Phase 17, FN-A6)."""

from __future__ import annotations

from typing import TYPE_CHECKING

import pytest

from mgf.vm.forensics.analysis.ioc import (
    DEFAULT_MIN_STRING_LENGTH,
    FileHashes,
    StringHit,
    YaraMatch,
    default_yara_rule_dir,
    extract_strings,
    hash_file,
    scan_yara,
)

if TYPE_CHECKING:
    from pathlib import Path


@pytest.fixture
def fake_home(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
    home = tmp_path / "home"
    home.mkdir()
    monkeypatch.setenv("HOME", str(home))
    return home


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------


class TestConstants:
    def test_min_string_length_default(self) -> None:
        assert DEFAULT_MIN_STRING_LENGTH == 6


class TestPathResolution:
    def test_lazy_yara_dir(self, fake_home: Path) -> None:
        path = default_yara_rule_dir()
        assert path == fake_home / ".local/share/vmanager/forensics/yara-rules"


# ---------------------------------------------------------------------------
# Strings
# ---------------------------------------------------------------------------


class TestExtractStrings:
    def test_extracts_ascii_string(self, tmp_path: Path) -> None:
        f = tmp_path / "x.bin"
        f.write_bytes(b"\x00\x01HelloWorld\x00\x02")
        hits = extract_strings(f, min_length=4)
        texts = [h.text for h in hits]
        assert "HelloWorld" in texts

    def test_min_length_filters(self, tmp_path: Path) -> None:
        f = tmp_path / "x.bin"
        f.write_bytes(b"\x00abc\x00abcdef\x00")
        hits = extract_strings(f, min_length=4)
        texts = [h.text for h in hits]
        # "abc" is below min_length; "abcdef" passes.
        assert "abcdef" in texts
        assert "abc" not in texts

    def test_pattern_filter(self, tmp_path: Path) -> None:
        f = tmp_path / "x.bin"
        f.write_bytes(b"\x00ftp://victim\x00not-a-url\x00http://other\x00")
        hits = extract_strings(f, min_length=4, pattern=r"^https?://")
        texts = [h.text for h in hits]
        assert "http://other" in texts
        assert "not-a-url" not in texts

    def test_max_hits_caps_results(self, tmp_path: Path) -> None:
        f = tmp_path / "x.bin"
        # 100 strings separated by null bytes.
        body = b"\x00".join(f"string{i:04d}".encode("ascii") for i in range(100))
        f.write_bytes(body)
        hits = extract_strings(f, min_length=4, max_hits=10)
        assert len(hits) <= 10

    def test_returns_string_hit(self, tmp_path: Path) -> None:
        f = tmp_path / "x.bin"
        f.write_bytes(b"\x00HelloThere\x00")
        hits = extract_strings(f, min_length=4)
        assert all(isinstance(h, StringHit) for h in hits)
        assert all(h.encoding == "ascii" for h in hits)

    def test_invalid_min_length_raises(self, tmp_path: Path) -> None:
        f = tmp_path / "x.bin"
        f.write_bytes(b"x")
        with pytest.raises(ValueError, match="min_length"):
            extract_strings(f, min_length=0)


# ---------------------------------------------------------------------------
# Hashes
# ---------------------------------------------------------------------------


class TestHashFile:
    def test_known_hashes_for_abc(self, tmp_path: Path) -> None:
        f = tmp_path / "x.txt"
        f.write_bytes(b"abc")
        h = hash_file(f)
        assert isinstance(h, FileHashes)
        assert h.size_bytes == 3
        # Known values for "abc":
        assert h.md5 == "900150983cd24fb0d6963f7d28e17f72"
        assert h.sha1 == "a9993e364706816aba3e25717850c26c9cd0d89d"
        assert h.sha256.startswith("ba7816bf")

    def test_empty_file(self, tmp_path: Path) -> None:
        f = tmp_path / "empty.bin"
        f.write_bytes(b"")
        h = hash_file(f)
        assert h.size_bytes == 0
        assert h.sha256 == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"


# ---------------------------------------------------------------------------
# YARA — gracefully handles missing dep
# ---------------------------------------------------------------------------


class TestScanYara:
    def test_missing_yara_returns_empty(self, monkeypatch: pytest.MonkeyPatch) -> None:
        # Force ImportError in the function's lazy import.
        import builtins

        original = builtins.__import__

        def fake_import(name: str, *args: object, **kwargs: object) -> object:
            if name == "yara":
                raise ImportError("forced")
            return original(name, *args, **kwargs)

        monkeypatch.setattr(builtins, "__import__", fake_import)
        result = scan_yara([])
        assert result == []

    def test_missing_rules_dir_returns_empty(
        self,
        fake_home: Path,
    ) -> None:
        # No rules dir created; scan returns empty.
        # Whether yara is installed or not, this path returns [].
        try:
            import yara  # noqa: F401
        except ImportError:
            pytest.skip("yara not installed")
        result = scan_yara([])
        assert result == []

    def test_yara_match_dataclass_shape(self) -> None:
        m = YaraMatch(file="/x", rule="my_rule", namespace="ns", tags=("malware",))
        assert m.rule == "my_rule"
        assert m.tags == ("malware",)
