"""tests/test_aibom.py — AIBoM capture unit tests (Phase 2.1)."""
import json
import tempfile
from pathlib import Path

import pytest

from sichgate.aibom_capture import (
    AIBoMCapture,
    HardwareContext,
    capture_model_aibom,
    detect_hardware,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def tmp_model_file(tmp_path: Path) -> str:
    """Create a small dummy model file for testing."""
    f = tmp_path / "model.gguf"
    f.write_bytes(b"FAKE_MODEL_WEIGHTS" * 100)
    return str(f)


@pytest.fixture
def aibom(tmp_model_file: str) -> AIBoMCapture:
    return AIBoMCapture(
        model_path=tmp_model_file,
        model_name="test-model",
        model_version="1.0",
        quantization_type="4-bit GGUF",
    )


# ---------------------------------------------------------------------------
# 1. Initialization
# ---------------------------------------------------------------------------

def test_aibom_init_sets_model_name(aibom):
    assert aibom.model_name == "test-model"


def test_aibom_init_sets_model_version(aibom):
    assert aibom.model_version == "1.0"


def test_aibom_test_run_id_is_uuid(aibom):
    import re
    pattern = r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
    assert re.match(pattern, aibom.test_run_id)


def test_aibom_unique_run_id_per_instance(tmp_model_file):
    a1 = AIBoMCapture(model_path=tmp_model_file, model_name="m", model_version="1")
    a2 = AIBoMCapture(model_path=tmp_model_file, model_name="m", model_version="1")
    assert a1.test_run_id != a2.test_run_id


def test_aibom_timestamp_is_iso_format(aibom):
    from datetime import datetime
    # Should parse without error
    datetime.fromisoformat(aibom.timestamp.replace("Z", "+00:00"))


# ---------------------------------------------------------------------------
# 2. File hashing
# ---------------------------------------------------------------------------

def test_aibom_sha256_computed_for_existing_file(aibom):
    assert aibom.model_sha256 is not None
    assert len(aibom.model_sha256) == 64


def test_aibom_sha256_is_none_for_nonexistent_path():
    a = AIBoMCapture(model_path="/nonexistent/model.bin", model_name="x", model_version="0")
    assert a.model_sha256 is None


def test_aibom_file_size_computed(aibom):
    assert aibom.model_file_size_bytes is not None
    assert aibom.model_file_size_bytes > 0


def test_aibom_format_inferred_from_extension(aibom):
    assert aibom.model_format == "gguf"


# ---------------------------------------------------------------------------
# 3. CycloneDX 1.6 schema compliance
# ---------------------------------------------------------------------------

def test_aibom_cyclonedx_bom_format(aibom):
    bom = aibom.to_cyclonedx()
    assert bom["bomFormat"] == "CycloneDX"


def test_aibom_cyclonedx_spec_version(aibom):
    bom = aibom.to_cyclonedx()
    assert bom["specVersion"] == "1.6"


def test_aibom_cyclonedx_version_is_int(aibom):
    bom = aibom.to_cyclonedx()
    assert bom["version"] == 1


def test_aibom_serial_number_uses_run_id(aibom):
    bom = aibom.to_cyclonedx()
    assert bom["serialNumber"] == f"urn:uuid:{aibom.test_run_id}"


def test_aibom_metadata_timestamp_present(aibom):
    bom = aibom.to_cyclonedx()
    assert "timestamp" in bom["metadata"]


def test_aibom_metadata_has_sichgate_tool(aibom):
    bom = aibom.to_cyclonedx()
    tools = bom["metadata"]["tools"]
    names = [t["name"] for t in tools]
    assert "SichGate Pro" in names


def test_aibom_component_type_is_ml_model(aibom):
    bom = aibom.to_cyclonedx()
    comp = bom["components"][0]
    assert comp["type"] == "machine-learning-model"


def test_aibom_component_has_name(aibom):
    bom = aibom.to_cyclonedx()
    comp = bom["components"][0]
    assert comp["name"] == "test-model"


def test_aibom_component_has_sha256_hash(aibom):
    bom = aibom.to_cyclonedx()
    comp = bom["components"][0]
    hashes = comp.get("hashes", [])
    sha_entries = [h for h in hashes if h["alg"] == "SHA-256"]
    assert len(sha_entries) == 1
    assert len(sha_entries[0]["content"]) == 64


def test_aibom_model_card_present(aibom):
    bom = aibom.to_cyclonedx()
    comp = bom["components"][0]
    assert "modelCard" in comp


def test_aibom_quantization_in_model_card(aibom):
    bom = aibom.to_cyclonedx()
    comp = bom["components"][0]
    params = comp["modelCard"]["modelParameters"]
    assert params.get("quantizationType") == "4-bit GGUF"


# ---------------------------------------------------------------------------
# 4. Hardware context in metadata
# ---------------------------------------------------------------------------

def test_aibom_hardware_cpu_model_in_metadata(aibom):
    bom = aibom.to_cyclonedx()
    hw_props = {p["name"]: p["value"] for p in bom["metadata"]["properties"]}
    assert "hardware:cpu_model" in hw_props


def test_aibom_hardware_ram_in_metadata(aibom):
    bom = aibom.to_cyclonedx()
    hw_props = {p["name"]: p["value"] for p in bom["metadata"]["properties"]}
    assert "hardware:ram_gb" in hw_props


def test_aibom_hardware_platform_in_metadata(aibom):
    bom = aibom.to_cyclonedx()
    hw_props = {p["name"]: p["value"] for p in bom["metadata"]["properties"]}
    assert "hardware:platform_os" in hw_props


# ---------------------------------------------------------------------------
# 5. Annex IV compliance mapping
# ---------------------------------------------------------------------------

def test_aibom_article11_sections_present(aibom):
    bom = aibom.to_cyclonedx()
    props = {p["name"]: p["value"] for p in bom["metadata"]["properties"]}
    assert "eu_ai_act:article_11_sections" in props


def test_aibom_article11_sections_include_1e(aibom):
    bom = aibom.to_cyclonedx()
    props = {p["name"]: p["value"] for p in bom["metadata"]["properties"]}
    sections = props["eu_ai_act:article_11_sections"].split(",")
    assert "1e" in sections


# ---------------------------------------------------------------------------
# 6. JSON serialisation and save/load
# ---------------------------------------------------------------------------

def test_aibom_to_json_valid(aibom):
    raw = aibom.to_json()
    parsed = json.loads(raw)
    assert parsed["bomFormat"] == "CycloneDX"


def test_aibom_save_creates_file(aibom, tmp_path):
    dest = str(tmp_path / "bom.json")
    aibom.save(dest)
    assert Path(dest).exists()
    parsed = json.loads(Path(dest).read_text())
    assert parsed["specVersion"] == "1.6"


def test_aibom_to_dict_equals_to_cyclonedx(aibom):
    assert aibom.to_dict() == aibom.to_cyclonedx()


# ---------------------------------------------------------------------------
# 7. Factory helper
# ---------------------------------------------------------------------------

def test_capture_model_aibom_infers_name(tmp_model_file):
    bom = capture_model_aibom(model_path=tmp_model_file)
    assert bom.model_name == "model"  # stem of "model.gguf"


def test_capture_model_aibom_with_explicit_name(tmp_model_file):
    bom = capture_model_aibom(model_path=tmp_model_file, model_name="my-custom-model")
    assert bom.model_name == "my-custom-model"


# ---------------------------------------------------------------------------
# 8. Hardware detection (smoke test)
# ---------------------------------------------------------------------------

def test_detect_hardware_returns_hardware_context():
    hw = detect_hardware()
    assert isinstance(hw, HardwareContext)
    assert hw.cpu_cores >= 1
    assert isinstance(hw.platform_os, str)
    assert len(hw.platform_os) > 0
