"""
Tests for GuideScan2 off-target scoring module.
"""

import os
import tempfile
import pytest
import numpy as np
import pandas as pd
from unittest.mock import patch, MagicMock

from crisprdesigner2 import score_guides_guidescan, compute_specificity_mock
from crisprdesigner2._guidescan_scoring import validate_inputs, SUPPORTED_GENOMES


class TestValidateInputs:
    """Tests for the validate_inputs function."""

    def test_missing_index_raises_error(self):
        """Missing index file should raise FileNotFoundError."""
        with pytest.raises(FileNotFoundError) as excinfo:
            validate_inputs('/nonexistent/path/to/index.db', 'hg38')
        assert "not found" in str(excinfo.value).lower()

    def test_unsupported_genome_raises_error(self):
        """Unsupported genome should raise ValueError."""
        # Create a temporary file to act as index
        with tempfile.NamedTemporaryFile(delete=False) as f:
            index_path = f.name

        try:
            with pytest.raises(ValueError) as excinfo:
                validate_inputs(index_path, 'hg19')
            assert "Unsupported genome" in str(excinfo.value)
            assert "hg19" in str(excinfo.value)
        finally:
            os.unlink(index_path)

    def test_valid_inputs_hg38(self):
        """Valid inputs for hg38 should not raise errors."""
        with tempfile.NamedTemporaryFile(delete=False) as f:
            index_path = f.name

        try:
            # Should not raise
            validate_inputs(index_path, 'hg38')
        finally:
            os.unlink(index_path)

    def test_valid_inputs_mm10(self):
        """Valid inputs for mm10 should not raise errors."""
        with tempfile.NamedTemporaryFile(delete=False) as f:
            index_path = f.name

        try:
            # Should not raise
            validate_inputs(index_path, 'mm10')
        finally:
            os.unlink(index_path)


class TestScoreGuidesGuidescan:
    """Tests for the score_guides_guidescan function."""

    @pytest.fixture
    def sample_guides_df(self):
        """Create a sample DataFrame of guides."""
        return pd.DataFrame({
            'protospacer': [
                'ACGTACGATCGATCGACGAA',
                'CGATCGATCGATCGATCGAC',
                'TACGAACGTTACGTAACGAC',
            ],
            'chr': ['chr1', 'chr1', 'chr2'],
            'start': [100, 200, 300],
            'end': [120, 220, 320],
            'strand': ['+', '-', '+']
        })

    def test_missing_index_raises_error(self, sample_guides_df):
        """Missing index file should raise FileNotFoundError."""
        with pytest.raises(FileNotFoundError):
            score_guides_guidescan(sample_guides_df, '/nonexistent/index.db', 'hg38')

    def test_unsupported_genome_raises_error(self, sample_guides_df):
        """Unsupported genome should raise ValueError."""
        with tempfile.NamedTemporaryFile(delete=False) as f:
            index_path = f.name

        try:
            with pytest.raises(ValueError) as excinfo:
                score_guides_guidescan(sample_guides_df, index_path, 'hg19')
            assert "Unsupported genome" in str(excinfo.value)
        finally:
            os.unlink(index_path)

    def test_with_mocked_guidescan_api(self, sample_guides_df):
        """Test with mocked GuideScan2 Python API."""
        with tempfile.NamedTemporaryFile(delete=False) as f:
            index_path = f.name

        try:
            # Mock the guidescan module
            mock_db = MagicMock()
            mock_db.query.return_value = {'specificity': 0.85}

            mock_guidescan = MagicMock()
            mock_guidescan.GuideScanDB.return_value = mock_db

            with patch.dict('sys.modules', {'guidescan': mock_guidescan}):
                # Need to reimport to pick up the mock
                import importlib
                import crisprdesigner2._guidescan_scoring as guidescan_scoring
                importlib.reload(guidescan_scoring)

                result = guidescan_scoring.score_guides_guidescan(
                    sample_guides_df, index_path, 'hg38'
                )

                assert 'specificity_score' in result.columns
                assert len(result) == 3

        finally:
            os.unlink(index_path)
            # Reload to restore original state
            import importlib
            import crisprdesigner2._guidescan_scoring as guidescan_scoring
            importlib.reload(guidescan_scoring)

    def test_nan_for_missing_guides(self, sample_guides_df):
        """Guides missing from index should receive NaN."""
        with tempfile.NamedTemporaryFile(delete=False) as f:
            index_path = f.name

        try:
            # Mock the guidescan module to return None for one guide
            mock_db = MagicMock()
            # First guide has result, second and third return None
            mock_db.query.side_effect = [
                {'specificity': 0.85},
                None,
                {'specificity': 0.75}
            ]

            mock_guidescan = MagicMock()
            mock_guidescan.GuideScanDB.return_value = mock_db

            with patch.dict('sys.modules', {'guidescan': mock_guidescan}):
                import importlib
                import crisprdesigner2._guidescan_scoring as guidescan_scoring
                importlib.reload(guidescan_scoring)

                result = guidescan_scoring.score_guides_guidescan(
                    sample_guides_df, index_path, 'hg38'
                )

                assert 'specificity_score' in result.columns
                assert result['specificity_score'].iloc[0] == 0.85
                assert np.isnan(result['specificity_score'].iloc[1])
                assert result['specificity_score'].iloc[2] == 0.75

        finally:
            os.unlink(index_path)
            import importlib
            import crisprdesigner2._guidescan_scoring as guidescan_scoring
            importlib.reload(guidescan_scoring)

    def test_preserves_other_columns(self, sample_guides_df):
        """score_guides_guidescan should preserve other columns."""
        with tempfile.NamedTemporaryFile(delete=False) as f:
            index_path = f.name

        try:
            # Add an extra column
            df = sample_guides_df.copy()
            df['RS3_score'] = [0.8, 0.7, 0.6]

            # Mock the guidescan module
            mock_db = MagicMock()
            mock_db.query.return_value = {'specificity': 0.85}

            mock_guidescan = MagicMock()
            mock_guidescan.GuideScanDB.return_value = mock_db

            with patch.dict('sys.modules', {'guidescan': mock_guidescan}):
                import importlib
                import crisprdesigner2._guidescan_scoring as guidescan_scoring
                importlib.reload(guidescan_scoring)

                result = guidescan_scoring.score_guides_guidescan(df, index_path, 'hg38')

                assert 'chr' in result.columns
                assert 'start' in result.columns
                assert 'RS3_score' in result.columns
                assert result['chr'].iloc[0] == 'chr1'
                assert result['RS3_score'].iloc[0] == 0.8

        finally:
            os.unlink(index_path)
            import importlib
            import crisprdesigner2._guidescan_scoring as guidescan_scoring
            importlib.reload(guidescan_scoring)


class TestComputeSpecificityMock:
    """Tests for the mock specificity function."""

    @pytest.fixture
    def sample_guides_df(self):
        """Create a sample DataFrame of guides."""
        return pd.DataFrame({
            'protospacer': [
                'ACGTACGATCGATCGACGAA',
                'GCGCGCGCGCGCGCGCGCAA',  # Higher GC
                'ATATATATATATATATAAAA',  # Lower GC
            ]
        })

    def test_adds_specificity_score(self, sample_guides_df):
        """compute_specificity_mock should add specificity_score column."""
        result = compute_specificity_mock(sample_guides_df)
        assert 'specificity_score' in result.columns
        assert len(result) == 3

    def test_scores_in_valid_range(self, sample_guides_df):
        """Specificity scores should be between 0 and 1."""
        result = compute_specificity_mock(sample_guides_df)
        assert all(result['specificity_score'] >= 0.0)
        assert all(result['specificity_score'] <= 1.0)

    def test_preserves_other_columns(self, sample_guides_df):
        """compute_specificity_mock should preserve other columns."""
        df = sample_guides_df.copy()
        df['chr'] = ['chr1', 'chr2', 'chr3']

        result = compute_specificity_mock(df)

        assert 'chr' in result.columns
        assert result['chr'].iloc[0] == 'chr1'


class TestSupportedGenomes:
    """Tests for supported genome configurations."""

    def test_hg38_supported(self):
        """hg38 should be in supported genomes."""
        assert 'hg38' in SUPPORTED_GENOMES

    def test_mm10_supported(self):
        """mm10 should be in supported genomes."""
        assert 'mm10' in SUPPORTED_GENOMES


if __name__ == '__main__':
    pytest.main([__file__, '-v'])
