"""Unit tests for database models and connection management.

Tests SQLAlchemy models, relationships, session management, and database
operations. Uses an in-memory SQLite database for isolated testing.
"""
from __future__ import annotations

from datetime import datetime, timedelta, timezone

import pytest
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session, sessionmaker

from src.server.database.base import Base, SoftDeleteMixin, TimestampMixin
from src.server.database.models import (
    AnimeSeries,
    DownloadQueueItem,
    Episode,
    UserSession,
)


@pytest.fixture
def db_engine():
    """Create in-memory SQLite database engine for testing."""
    engine = create_engine("sqlite:///:memory:", echo=False)
    Base.metadata.create_all(engine)
    return engine


@pytest.fixture
def db_session(db_engine):
    """Create database session for testing."""
    SessionLocal = sessionmaker(bind=db_engine)
    session = SessionLocal()
    yield session
    session.close()


class TestAnimeSeries:
    """Test cases for AnimeSeries model."""

    def test_create_anime_series(self, db_session: Session):
        """Test creating an anime series."""
        series = AnimeSeries(
            key="attack-on-titan",
            name="Attack on Titan",
            site="https://aniworld.to",
            folder="/anime/attack-on-titan",
        )
        
        db_session.add(series)
        db_session.commit()
        
        # Verify saved
        assert series.id is not None
        assert series.key == "attack-on-titan"
        assert series.name == "Attack on Titan"
        assert series.created_at is not None
        assert series.updated_at is not None

    def test_anime_series_unique_key(self, db_session: Session):
        """Test that series key must be unique."""
        series1 = AnimeSeries(
            key="unique-key",
            name="Series 1",
            site="https://example.com",
            folder="/anime/series1",
        )
        series2 = AnimeSeries(
            key="unique-key",
            name="Series 2",
            site="https://example.com",
            folder="/anime/series2",
        )
        
        db_session.add(series1)
        db_session.commit()
        
        db_session.add(series2)
        with pytest.raises(Exception):  # IntegrityError
            db_session.commit()

    def test_anime_series_relationships(self, db_session: Session):
        """Test relationships with episodes and download items."""
        series = AnimeSeries(
            key="test-series",
            name="Test Series",
            site="https://example.com",
            folder="/anime/test",
        )
        db_session.add(series)
        db_session.commit()
        
        # Add episodes
        episode1 = Episode(
            series_id=series.id,
            season=1,
            episode_number=1,
            title="Episode 1",
        )
        episode2 = Episode(
            series_id=series.id,
            season=1,
            episode_number=2,
            title="Episode 2",
        )
        db_session.add_all([episode1, episode2])
        db_session.commit()
        
        # Verify relationship
        assert len(series.episodes) == 2
        assert series.episodes[0].title == "Episode 1"

    def test_anime_series_cascade_delete(self, db_session: Session):
        """Test that deleting series cascades to episodes."""
        series = AnimeSeries(
            key="cascade-test",
            name="Cascade Test",
            site="https://example.com",
            folder="/anime/cascade",
        )
        db_session.add(series)
        db_session.commit()
        
        # Add episodes
        episode = Episode(
            series_id=series.id,
            season=1,
            episode_number=1,
        )
        db_session.add(episode)
        db_session.commit()
        
        series_id = series.id
        
        # Delete series
        db_session.delete(series)
        db_session.commit()
        
        # Verify episodes are deleted
        result = db_session.execute(
            select(Episode).where(Episode.series_id == series_id)
        )
        assert result.scalar_one_or_none() is None

    def test_anime_series_nfo_fields_default_values(self, db_session: Session):
        """Test NFO fields have correct default values."""
        series = AnimeSeries(
            key="nfo-test",
            name="NFO Test Series",
            site="https://example.com",
            folder="/anime/nfo-test",
        )
        db_session.add(series)
        db_session.commit()
        
        # Verify NFO fields default values
        assert series.has_nfo is False
        assert series.nfo_created_at is None
        assert series.nfo_updated_at is None
        assert series.tmdb_id is None
        assert series.tvdb_id is None

    def test_anime_series_nfo_fields_set_values(self, db_session: Session):
        """Test setting NFO field values."""
        now = datetime.now(timezone.utc)
        
        series = AnimeSeries(
            key="nfo-values-test",
            name="NFO Values Test",
            site="https://example.com",
            folder="/anime/nfo-values",
            has_nfo=True,
            nfo_created_at=now,
            nfo_updated_at=now,
            tmdb_id=12345,
            tvdb_id=67890,
        )
        db_session.add(series)
        db_session.commit()
        
        # Verify NFO fields are saved
        assert series.has_nfo is True
        assert series.nfo_created_at is not None
        assert series.nfo_updated_at is not None
        # Check time is close (within 1 second)
        # Timezone may be lost in SQLite
        created_delta = abs(
            (series.nfo_created_at.replace(tzinfo=timezone.utc) - now)
            .total_seconds()
        )
        updated_delta = abs(
            (series.nfo_updated_at.replace(tzinfo=timezone.utc) - now)
            .total_seconds()
        )
        assert created_delta < 1
        assert updated_delta < 1
        assert series.tmdb_id == 12345
        assert series.tvdb_id == 67890

    def test_anime_series_update_nfo_status(self, db_session: Session):
        """Test updating NFO status fields."""
        series = AnimeSeries(
            key="nfo-update-test",
            name="NFO Update Test",
            site="https://example.com",
            folder="/anime/nfo-update",
        )
        db_session.add(series)
        db_session.commit()
        
        # Initially no NFO
        assert series.has_nfo is False
        
        # Update NFO status
        now = datetime.now(timezone.utc)
        series.has_nfo = True
        series.nfo_created_at = now
        series.nfo_updated_at = now
        series.tmdb_id = 99999
        db_session.commit()
        
        # Verify update
        db_session.refresh(series)
        assert series.has_nfo is True
        assert series.nfo_created_at is not None
        assert series.nfo_updated_at is not None
        assert series.tmdb_id == 99999

    def test_anime_series_query_by_nfo_status(self, db_session: Session):
        """Test querying series by NFO status."""
        # Create series with and without NFO
        series_with_nfo = AnimeSeries(
            key="with-nfo",
            name="Series With NFO",
            site="https://example.com",
            folder="/anime/with-nfo",
            has_nfo=True,
            tmdb_id=111,
        )
        series_without_nfo = AnimeSeries(
            key="without-nfo",
            name="Series Without NFO",
            site="https://example.com",
            folder="/anime/without-nfo",
            has_nfo=False,
        )
        
        db_session.add_all([series_with_nfo, series_without_nfo])
        db_session.commit()
        
        # Query series with NFO
        with_nfo = db_session.execute(
            select(AnimeSeries).where(
                AnimeSeries.has_nfo == True  # noqa: E712
            )
        ).scalars().all()
        assert len(with_nfo) == 1
        assert with_nfo[0].key == "with-nfo"
        
        # Query series without NFO
        without_nfo = db_session.execute(
            select(AnimeSeries).where(
                AnimeSeries.has_nfo == False  # noqa: E712
            )
        ).scalars().all()
        assert len(without_nfo) == 1
        assert without_nfo[0].key == "without-nfo"

    def test_anime_series_query_by_tmdb_id(self, db_session: Session):
        """Test querying series by TMDB ID."""
        series1 = AnimeSeries(
            key="tmdb-test-1",
            name="TMDB Test 1",
            site="https://example.com",
            folder="/anime/tmdb-1",
            tmdb_id=12345,
        )
        series2 = AnimeSeries(
            key="tmdb-test-2",
            name="TMDB Test 2",
            site="https://example.com",
            folder="/anime/tmdb-2",
            tmdb_id=67890,
        )
        series3 = AnimeSeries(
            key="tmdb-test-3",
            name="TMDB Test 3",
            site="https://example.com",
            folder="/anime/tmdb-3",
        )
        
        db_session.add_all([series1, series2, series3])
        db_session.commit()
        
        # Query by specific TMDB ID
        result = db_session.execute(
            select(AnimeSeries).where(AnimeSeries.tmdb_id == 12345)
        ).scalar_one_or_none()
        assert result is not None
        assert result.key == "tmdb-test-1"
        
        # Query series with any TMDB ID
        with_tmdb = db_session.execute(
            select(AnimeSeries).where(AnimeSeries.tmdb_id.isnot(None))
        ).scalars().all()
        assert len(with_tmdb) == 2


class TestEpisode:
    """Test cases for Episode model."""

    def test_create_episode(self, db_session: Session):
        """Test creating an episode."""
        series = AnimeSeries(
            key="test-series",
            name="Test Series",
            site="https://example.com",
            folder="/anime/test",
        )
        db_session.add(series)
        db_session.commit()
        
        episode = Episode(
            series_id=series.id,
            season=1,
            episode_number=5,
            title="The Fifth Episode",
            file_path="/anime/test/S01E05.mp4",
            is_downloaded=True,
        )
        
        db_session.add(episode)
        db_session.commit()
        
        # Verify saved
        assert episode.id is not None
        assert episode.season == 1
        assert episode.episode_number == 5
        assert episode.is_downloaded is True
        assert episode.created_at is not None

    def test_episode_relationship_to_series(self, db_session: Session):
        """Test episode relationship to series."""
        series = AnimeSeries(
            key="relationship-test",
            name="Relationship Test",
            site="https://example.com",
            folder="/anime/relationship",
        )
        db_session.add(series)
        db_session.commit()
        
        episode = Episode(
            series_id=series.id,
            season=1,
            episode_number=1,
        )
        db_session.add(episode)
        db_session.commit()
        
        # Verify relationship
        assert episode.series.name == "Relationship Test"
        assert episode.series.key == "relationship-test"


class TestDownloadQueueItem:
    """Test cases for DownloadQueueItem model."""

    def test_create_download_item(self, db_session: Session):
        """Test creating a download queue item."""
        series = AnimeSeries(
            key="download-test",
            name="Download Test",
            site="https://example.com",
            folder="/anime/download",
        )
        db_session.add(series)
        db_session.commit()
        
        episode = Episode(
            series_id=series.id,
            season=1,
            episode_number=3,
        )
        db_session.add(episode)
        db_session.commit()
        
        item = DownloadQueueItem(
            series_id=series.id,
            episode_id=episode.id,
            download_url="https://example.com/download/ep3",
            file_destination="/anime/download/S01E03.mp4",
        )
        
        db_session.add(item)
        db_session.commit()
        
        # Verify saved
        assert item.id is not None
        assert item.episode_id == episode.id
        assert item.series_id == series.id

    def test_download_item_episode_relationship(self, db_session: Session):
        """Test download item episode relationship."""
        series = AnimeSeries(
            key="relationship-test",
            name="Relationship Test",
            site="https://example.com",
            folder="/anime/relationship",
        )
        db_session.add(series)
        db_session.commit()
        
        episode = Episode(
            series_id=series.id,
            season=1,
            episode_number=1,
        )
        db_session.add(episode)
        db_session.commit()
        
        item = DownloadQueueItem(
            series_id=series.id,
            episode_id=episode.id,
        )
        db_session.add(item)
        db_session.commit()
        
        # Verify relationship
        assert item.episode.id == episode.id
        assert item.series.id == series.id

    def test_download_item_error_handling(self, db_session: Session):
        """Test download item with error information."""
        series = AnimeSeries(
            key="error-test",
            name="Error Test",
            site="https://example.com",
            folder="/anime/error",
        )
        db_session.add(series)
        db_session.commit()
        
        episode = Episode(
            series_id=series.id,
            season=1,
            episode_number=1,
        )
        db_session.add(episode)
        db_session.commit()
        
        item = DownloadQueueItem(
            series_id=series.id,
            episode_id=episode.id,
            error_message="Network timeout after 30 seconds",
        )
        db_session.add(item)
        db_session.commit()
        
        # Verify error info
        assert item.error_message == "Network timeout after 30 seconds"


class TestUserSession:
    """Test cases for UserSession model."""

    def test_create_user_session(self, db_session: Session):
        """Test creating a user session."""
        expires = datetime.now(timezone.utc) + timedelta(hours=24)
        
        session = UserSession(
            session_id="test-session-123",
            token_hash="hashed-token-value",
            user_id="user-1",
            ip_address="192.168.1.100",
            user_agent="Mozilla/5.0",
            expires_at=expires,
            is_active=True,
        )
        
        db_session.add(session)
        db_session.commit()
        
        # Verify saved
        assert session.id is not None
        assert session.session_id == "test-session-123"
        assert session.is_active is True
        assert session.created_at is not None

    def test_session_unique_session_id(self, db_session: Session):
        """Test that session_id must be unique."""
        expires = datetime.now(timezone.utc) + timedelta(hours=24)
        
        session1 = UserSession(
            session_id="duplicate-id",
            token_hash="hash1",
            expires_at=expires,
        )
        session2 = UserSession(
            session_id="duplicate-id",
            token_hash="hash2",
            expires_at=expires,
        )
        
        db_session.add(session1)
        db_session.commit()
        
        db_session.add(session2)
        with pytest.raises(Exception):  # IntegrityError
            db_session.commit()

    def test_session_is_expired(self, db_session: Session):
        """Test session expiration check."""
        # Create expired session
        expired = datetime.now(timezone.utc) - timedelta(hours=1)
        session = UserSession(
            session_id="expired-session",
            token_hash="hash",
            expires_at=expired,
        )
        
        db_session.add(session)
        db_session.commit()
        
        # Verify is_expired
        assert session.is_expired is True

    def test_session_revoke(self, db_session: Session):
        """Test session revocation."""
        expires = datetime.now(timezone.utc) + timedelta(hours=24)
        session = UserSession(
            session_id="revoke-test",
            token_hash="hash",
            expires_at=expires,
            is_active=True,
        )
        
        db_session.add(session)
        db_session.commit()
        
        # Revoke session
        session.revoke()
        db_session.commit()
        
        # Verify revoked
        assert session.is_active is False


class TestTimestampMixin:
    """Test cases for TimestampMixin."""

    def test_timestamp_auto_creation(self, db_session: Session):
        """Test that timestamps are automatically created."""
        series = AnimeSeries(
            key="timestamp-test",
            name="Timestamp Test",
            site="https://example.com",
            folder="/anime/timestamp",
        )
        
        db_session.add(series)
        db_session.commit()
        
        # Verify timestamps exist
        assert series.created_at is not None
        assert series.updated_at is not None
        assert series.created_at == series.updated_at

    def test_timestamp_auto_update(self, db_session: Session):
        """Test that updated_at is automatically updated."""
        series = AnimeSeries(
            key="update-test",
            name="Update Test",
            site="https://example.com",
            folder="/anime/update",
        )
        
        db_session.add(series)
        db_session.commit()
        
        original_updated = series.updated_at
        
        # Update and save
        series.name = "Updated Name"
        db_session.commit()
        
        # Verify updated_at changed
        # Note: This test may be flaky due to timing
        assert series.created_at is not None


class TestSoftDeleteMixin:
    """Test cases for SoftDeleteMixin."""

    def test_soft_delete_not_applied_to_models(self):
        """Test that SoftDeleteMixin is not applied to current models.
        
        This is a documentation test - models don't currently use
        SoftDeleteMixin, but it's available for future use.
        """
        # Verify models don't have deleted_at attribute
        series = AnimeSeries(
            key="soft-delete-test",
            name="Soft Delete Test",
            site="https://example.com",
            folder="/anime/soft-delete",
        )
        
        # Models shouldn't have soft delete attributes
        assert not hasattr(series, "deleted_at")
        assert not hasattr(series, "is_deleted")
        assert not hasattr(series, "soft_delete")


class TestDatabaseQueries:
    """Test complex database queries and operations."""

    def test_query_series_with_episodes(self, db_session: Session):
        """Test querying series with their episodes."""
        # Create series with episodes
        series = AnimeSeries(
            key="query-test",
            name="Query Test",
            site="https://example.com",
            folder="/anime/query",
        )
        db_session.add(series)
        db_session.commit()
        
        # Add multiple episodes
        for i in range(1, 6):
            episode = Episode(
                series_id=series.id,
                season=1,
                episode_number=i,
                title=f"Episode {i}",
            )
            db_session.add(episode)
        db_session.commit()
        
        # Query series with episodes
        result = db_session.execute(
            select(AnimeSeries).where(AnimeSeries.key == "query-test")
        )
        queried_series = result.scalar_one()
        
        # Verify episodes loaded
        assert len(queried_series.episodes) == 5

    def test_query_download_queue_by_status(self, db_session: Session):
        """Test querying download queue by status."""
        series = AnimeSeries(
            key="queue-query-test",
            name="Queue Query Test",
            site="https://example.com",
            folder="/anime/queue-query",
        )
        db_session.add(series)
        db_session.commit()
        
        # Create episodes and items
        for i in range(3):
            episode = Episode(
                series_id=series.id,
                season=1,
                episode_number=i + 1,
            )
            db_session.add(episode)
            db_session.commit()
            
            item = DownloadQueueItem(
                series_id=series.id,
                episode_id=episode.id,
            )
            db_session.add(item)
        db_session.commit()
        
        # Query all items
        result = db_session.execute(
            select(DownloadQueueItem)
        )
        items = result.scalars().all()
        
        # Verify query
        assert len(items) == 3

    def test_query_active_sessions(self, db_session: Session):
        """Test querying active user sessions."""
        expires = datetime.now(timezone.utc) + timedelta(hours=24)
        
        # Create active and inactive sessions
        active = UserSession(
            session_id="active-1",
            token_hash="hash1",
            expires_at=expires,
            is_active=True,
        )
        inactive = UserSession(
            session_id="inactive-1",
            token_hash="hash2",
            expires_at=expires,
            is_active=False,
        )
        
        db_session.add_all([active, inactive])
        db_session.commit()
        
        # Query active sessions
        result = db_session.execute(
            select(UserSession).where(UserSession.is_active == True)
        )
        active_sessions = result.scalars().all()
        
        # Verify query
        assert len(active_sessions) == 1
        assert active_sessions[0].session_id == "active-1"
