"""Tests for Spec-Driven Development spec tracker."""

import json
import time
from pathlib import Path

import pytest

from abi_spec.sdd import DriftResult, SpecTracker, SpecVersion


@pytest.fixture
def tmp_tracking_dir(tmp_path):
    """Create a temporary tracking directory."""
    tracking_dir = tmp_path / ".abi"
    tracking_dir.mkdir(parents=True, exist_ok=True)
    return tracking_dir


@pytest.fixture
def spec_tracker(tmp_tracking_dir):
    """Create a spec tracker with temporary tracking directory."""
    return SpecTracker(tracking_dir=tmp_tracking_dir)


@pytest.fixture
def spec_file(tmp_path):
    """Create a temporary spec file."""
    spec = tmp_path / "feature.md"
    spec.write_text("# Feature Spec\n\nThis is a test spec.", encoding="utf-8")
    return spec


@pytest.fixture
def code_file(tmp_path):
    """Create a temporary code file."""
    code = tmp_path / "feature.py"
    code.write_text("# Feature implementation\ndef feature():\n    pass", encoding="utf-8")
    return code


def test_spec_tracker_initialization(tmp_tracking_dir):
    """Test SpecTracker initialization."""
    tracker = SpecTracker(tracking_dir=tmp_tracking_dir)
    assert tracker.tracking_dir == tmp_tracking_dir
    assert tracker.tracking_file == tmp_tracking_dir / "spec_tracking.json"
    assert tracker.tracking_dir.exists()


def test_track_spec(spec_tracker, spec_file, code_file):
    """Test tracking a specification."""
    spec_version = spec_tracker.track(str(spec_file), [str(code_file)])

    assert isinstance(spec_version, SpecVersion)
    assert spec_version.spec_id == str(spec_file)
    assert spec_version.content_hash
    assert len(spec_version.content_hash) == 64  # SHA-256 hex length
    assert spec_version.updated_at
    assert spec_version.linked_code_paths == [str(code_file)]

    # Verify tracking data was saved
    tracking_file = spec_tracker.tracking_file
    assert tracking_file.exists()

    tracking_data = json.loads(tracking_file.read_text(encoding="utf-8"))
    assert str(spec_file) in tracking_data
    assert tracking_data[str(spec_file)]["content_hash"] == spec_version.content_hash


def test_track_nonexistent_spec(spec_tracker, tmp_path):
    """Test tracking a nonexistent spec raises FileNotFoundError."""
    nonexistent = tmp_path / "nonexistent.md"

    with pytest.raises(FileNotFoundError, match="Spec file not found"):
        spec_tracker.track(str(nonexistent), [])


def test_detect_drift_no_drift(spec_tracker, spec_file, code_file):
    """Test drift detection when spec and code are in sync."""
    # Track the spec first
    spec_tracker.track(str(spec_file), [str(code_file)])

    # Detect drift immediately - should show no drift since nothing changed
    # after tracking
    result = spec_tracker.detect_drift(str(spec_file), [str(code_file)])

    assert isinstance(result, DriftResult)
    assert result.has_drift is False
    assert result.spec_changed_since_code is False
    assert result.code_changed_since_spec is False
    assert "in sync" in result.details.lower()


def test_detect_drift_spec_newer_than_code(spec_tracker, spec_file, code_file):
    """Test drift detection when spec is newer than code."""
    # Track the spec first
    spec_tracker.track(str(spec_file), [str(code_file)])

    # Small delay to ensure different timestamps
    time.sleep(0.1)

    # Update the spec file
    spec_file.write_text("# Updated Feature Spec\n\nThis is updated.", encoding="utf-8")

    # Detect drift
    result = spec_tracker.detect_drift(str(spec_file), [str(code_file)])

    assert result.has_drift is True
    assert result.spec_changed_since_code is True
    assert "spec updated" in result.details.lower()
    assert "code may need regeneration" in result.details.lower()


def test_detect_drift_code_newer_than_spec(spec_tracker, spec_file, code_file):
    """Test drift detection when code is newer than spec."""
    # Track the spec first
    spec_tracker.track(str(spec_file), [str(code_file)])

    # Small delay to ensure different timestamps
    time.sleep(0.1)

    # Update the code file (without updating spec)
    code_file.write_text(
        "# Updated feature implementation\ndef feature():\n    return 'updated'",
        encoding="utf-8",
    )

    # Detect drift
    result = spec_tracker.detect_drift(str(spec_file), [str(code_file)])

    assert result.has_drift is True
    assert result.code_changed_since_spec is True
    assert "code updated without spec update" in result.details.lower()
    assert "drift detected" in result.details.lower()


def test_detect_drift_both_changed(spec_tracker, spec_file, code_file):
    """Test drift detection when both spec and code changed independently."""
    # Track the spec first with original timestamps
    original_spec_time = spec_file.stat().st_mtime
    original_code_time = code_file.stat().st_mtime
    spec_tracker.track(str(spec_file), [str(code_file)])

    # Update both files with different timestamps
    time.sleep(0.1)

    # Update code first
    code_file.write_text(
        "# Updated code\ndef feature():\n    return 'updated'",
        encoding="utf-8",
    )

    time.sleep(0.1)

    # Then update spec
    spec_file.write_text("# Updated Spec\n\nBoth changed.", encoding="utf-8")

    # Detect drift
    result = spec_tracker.detect_drift(str(spec_file), [str(code_file)])

    # Both spec and code are newer than each other at different points
    assert result.has_drift is True
    assert "manual reconciliation needed" in result.details.lower()


def test_detect_drift_untracked_spec(spec_tracker, spec_file, code_file):
    """Test drift detection for an untracked spec."""
    result = spec_tracker.detect_drift(str(spec_file), [str(code_file)])

    assert result.has_drift is False
    assert "not yet tracked" in result.details.lower()


def test_detect_drift_no_code_files(spec_tracker, spec_file):
    """Test drift detection when no code files exist."""
    spec_tracker.track(str(spec_file), [])

    result = spec_tracker.detect_drift(str(spec_file), [])

    assert result.has_drift is False
    assert "no code files found" in result.details.lower()


def test_detect_drift_multiple_code_files(spec_tracker, spec_file, tmp_path):
    """Test drift detection with multiple code files."""
    # Create multiple code files
    code1 = tmp_path / "feature1.py"
    code2 = tmp_path / "feature2.py"
    code1.write_text("def feature1(): pass", encoding="utf-8")
    code2.write_text("def feature2(): pass", encoding="utf-8")

    # Track the spec
    spec_tracker.track(str(spec_file), [str(code1), str(code2)])

    time.sleep(0.1)

    # Update one code file
    code1.write_text("def feature1(): return 'updated'", encoding="utf-8")

    # Detect drift - should detect that code changed
    result = spec_tracker.detect_drift(str(spec_file), [str(code1), str(code2)])

    assert result.has_drift is True
    assert result.code_changed_since_spec is True


def test_detect_drift_code_directory(spec_tracker, spec_file, tmp_path):
    """Test drift detection with a code directory."""
    # Create a code directory with files
    code_dir = tmp_path / "src"
    code_dir.mkdir()
    (code_dir / "feature1.py").write_text("def feature1(): pass", encoding="utf-8")
    (code_dir / "feature2.py").write_text("def feature2(): pass", encoding="utf-8")

    # Track the spec
    spec_tracker.track(str(spec_file), [str(code_dir)])

    time.sleep(0.1)

    # Update a file in the directory
    (code_dir / "feature1.py").write_text("def feature1(): return 'updated'", encoding="utf-8")

    # Detect drift
    result = spec_tracker.detect_drift(str(spec_file), [str(code_dir)])

    assert result.has_drift is True
    assert result.code_changed_since_spec is True


def test_content_hash_tracking(spec_tracker, spec_file, code_file):
    """Test that content hash is computed correctly and tracked."""
    spec_version = spec_tracker.track(str(spec_file), [str(code_file)])

    # Manually compute hash to verify
    import hashlib

    sha256 = hashlib.sha256()
    with spec_file.open("rb") as f:
        sha256.update(f.read())
    expected_hash = sha256.hexdigest()

    assert spec_version.content_hash == expected_hash


def test_detect_drift_nonexistent_spec(spec_tracker, tmp_path):
    """Test drift detection with nonexistent spec raises FileNotFoundError."""
    nonexistent = tmp_path / "nonexistent.md"

    with pytest.raises(FileNotFoundError, match="Spec file not found"):
        spec_tracker.detect_drift(str(nonexistent), [])


def test_track_updates_existing_entry(spec_tracker, spec_file, code_file):
    """Test that tracking a spec again updates the existing entry."""
    # Track the spec first
    spec_version1 = spec_tracker.track(str(spec_file), [str(code_file)])

    time.sleep(0.1)

    # Modify the spec
    spec_file.write_text("# Updated Spec\n\nContent changed.", encoding="utf-8")

    # Track again
    spec_version2 = spec_tracker.track(str(spec_file), [str(code_file)])

    # Verify the hash changed
    assert spec_version1.content_hash != spec_version2.content_hash

    # Verify only one entry exists in tracking file
    tracking_data = spec_tracker._load_tracking_data()
    assert len([k for k in tracking_data.keys() if str(spec_file) in k]) == 1
