"""Tests for deepgym.exploit_db -- exploit pattern database CRUD."""

from pathlib import Path

import pytest

from deepgym.exploit_db import ExploitDB, ExploitRecord


@pytest.fixture()
def db(tmp_path: Path) -> ExploitDB:
    """Return an ExploitDB backed by a temporary SQLite file."""
    db = ExploitDB(db_path=tmp_path / 'test_exploits.db')
    yield db
    db.close()


@pytest.fixture()
def sample_record() -> ExploitRecord:
    """Return a sample ExploitRecord for testing."""
    return ExploitRecord(
        verifier_id='verifier_001',
        benchmark='test_bench',
        exploitable=True,
        patterns=['stdout_injection', 'exit_code_manipulation'],
        num_attacks=5,
        num_exploits=2,
        max_exploit_score=0.95,
        attacks=[{'type': 'stdout_injection', 'score': 0.95}],
        verifier_hash='abc123',
        attack_type='white-box',
    )


class TestExploitRecord:
    """Verify ExploitRecord Pydantic model."""

    def test_defaults(self) -> None:
        record = ExploitRecord(
            verifier_id='v1',
            benchmark='bench',
            exploitable=False,
        )
        assert record.patterns == []
        assert record.num_attacks == 0
        assert record.num_exploits == 0
        assert record.max_exploit_score == 0.0
        assert record.attacks == []
        assert record.verifier_hash == ''
        assert record.attack_type == 'white-box'
        assert record.tested_at  # should be auto-populated

    def test_custom_values(self, sample_record: ExploitRecord) -> None:
        assert sample_record.verifier_id == 'verifier_001'
        assert sample_record.exploitable is True
        assert len(sample_record.patterns) == 2


class TestExploitDBCreation:
    """Verify ExploitDB construction and table creation."""

    def test_creation_in_temp_dir(self, tmp_path: Path) -> None:
        db = ExploitDB(db_path=tmp_path / 'test.db')
        assert (tmp_path / 'test.db').exists()
        db.close()

    def test_creates_results_table(self, db: ExploitDB) -> None:
        # Verify the table exists by querying it.
        rows = db.get_all()
        assert isinstance(rows, list)
        assert len(rows) == 0


class TestExploitDBSaveAndGet:
    """Verify save and retrieval operations."""

    def test_save_and_get(self, db: ExploitDB, sample_record: ExploitRecord) -> None:
        db.save(sample_record)
        retrieved = db.get('verifier_001', 'white-box')
        assert retrieved is not None
        assert retrieved.verifier_id == 'verifier_001'
        assert retrieved.exploitable is True
        assert retrieved.benchmark == 'test_bench'
        assert retrieved.num_exploits == 2
        assert retrieved.max_exploit_score == 0.95

    def test_get_nonexistent_returns_none(self, db: ExploitDB) -> None:
        assert db.get('does_not_exist') is None

    def test_save_updates_existing(self, db: ExploitDB, sample_record: ExploitRecord) -> None:
        db.save(sample_record)

        # Update the record.
        updated = ExploitRecord(
            verifier_id='verifier_001',
            benchmark='test_bench',
            exploitable=False,
            num_attacks=10,
            num_exploits=0,
            max_exploit_score=0.0,
            attack_type='white-box',
        )
        db.save(updated)

        retrieved = db.get('verifier_001', 'white-box')
        assert retrieved is not None
        assert retrieved.exploitable is False
        assert retrieved.num_attacks == 10

    def test_save_different_attack_types(self, db: ExploitDB) -> None:
        white = ExploitRecord(
            verifier_id='v1', benchmark='b', exploitable=True, attack_type='white-box'
        )
        black = ExploitRecord(
            verifier_id='v1', benchmark='b', exploitable=False, attack_type='black-box'
        )
        db.save(white)
        db.save(black)

        assert db.get('v1', 'white-box') is not None
        assert db.get('v1', 'white-box').exploitable is True
        assert db.get('v1', 'black-box') is not None
        assert db.get('v1', 'black-box').exploitable is False


class TestExploitDBGetAll:
    """Verify bulk retrieval with filters."""

    def test_get_all_empty(self, db: ExploitDB) -> None:
        assert db.get_all() == []

    def test_get_all_returns_saved(self, db: ExploitDB) -> None:
        for i in range(3):
            db.save(ExploitRecord(verifier_id=f'v{i}', benchmark='bench', exploitable=i % 2 == 0))
        results = db.get_all()
        assert len(results) == 3

    def test_get_all_filter_by_benchmark(self, db: ExploitDB) -> None:
        db.save(ExploitRecord(verifier_id='v1', benchmark='a', exploitable=True))
        db.save(ExploitRecord(verifier_id='v2', benchmark='b', exploitable=False))

        results_a = db.get_all(benchmark='a')
        assert len(results_a) == 1
        assert results_a[0].verifier_id == 'v1'

    def test_get_all_filter_by_attack_type(self, db: ExploitDB) -> None:
        db.save(
            ExploitRecord(
                verifier_id='v1', benchmark='b', exploitable=True, attack_type='white-box'
            )
        )
        db.save(
            ExploitRecord(
                verifier_id='v2', benchmark='b', exploitable=False, attack_type='black-box'
            )
        )

        white = db.get_all(attack_type='white-box')
        assert len(white) == 1
        assert white[0].verifier_id == 'v1'


class TestExploitDBStats:
    """Verify aggregate statistics."""

    def test_stats_empty(self, db: ExploitDB) -> None:
        stats = db.stats()
        assert stats['total_tested'] == 0
        assert stats['total_exploitable'] == 0
        assert stats['exploit_rate'] == 0.0

    def test_stats_with_data(self, db: ExploitDB) -> None:
        db.save(
            ExploitRecord(
                verifier_id='v1',
                benchmark='bench',
                exploitable=True,
                patterns=['injection'],
            )
        )
        db.save(
            ExploitRecord(
                verifier_id='v2',
                benchmark='bench',
                exploitable=False,
            )
        )

        stats = db.stats()
        assert stats['total_tested'] == 2
        assert stats['total_exploitable'] == 1
        assert stats['exploit_rate'] == 0.5
        assert 'by_benchmark' in stats
        assert 'bench' in stats['by_benchmark']

    def test_stats_by_benchmark_breakdown(self, db: ExploitDB) -> None:
        db.save(ExploitRecord(verifier_id='v1', benchmark='a', exploitable=True))
        db.save(ExploitRecord(verifier_id='v2', benchmark='a', exploitable=True))
        db.save(ExploitRecord(verifier_id='v3', benchmark='b', exploitable=False))

        stats = db.stats()
        assert stats['by_benchmark']['a']['tested'] == 2
        assert stats['by_benchmark']['a']['exploitable'] == 2
        assert stats['by_benchmark']['a']['exploit_rate'] == 1.0
        assert stats['by_benchmark']['b']['tested'] == 1
        assert stats['by_benchmark']['b']['exploit_rate'] == 0.0


class TestExploitDBPatternFrequency:
    """Verify pattern frequency counting."""

    def test_pattern_frequency_empty(self, db: ExploitDB) -> None:
        assert db.pattern_frequency() == {}

    def test_pattern_frequency_counts(self, db: ExploitDB) -> None:
        db.save(
            ExploitRecord(
                verifier_id='v1',
                benchmark='b',
                exploitable=True,
                patterns=['injection', 'overflow'],
            )
        )
        db.save(
            ExploitRecord(
                verifier_id='v2',
                benchmark='b',
                exploitable=True,
                patterns=['injection'],
            )
        )

        freq = db.pattern_frequency()
        assert freq['injection'] == 2
        assert freq['overflow'] == 1
