"""Unit tests for database initialization module.

Tests cover:
- Database initialization
- Schema creation and validation
- Schema version management
- Initial data seeding
- Health checks
- Backup functionality
"""
import logging
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
from sqlalchemy.pool import StaticPool

from src.server.database.base import Base
from src.server.database.init import (
    CURRENT_SCHEMA_VERSION,
    EXPECTED_TABLES,
    check_database_health,
    create_database_backup,
    create_database_schema,
    get_database_info,
    get_schema_version,
    initialize_database,
    seed_initial_data,
    validate_database_schema,
)


@pytest.fixture
async def test_engine():
    """Create in-memory SQLite engine for testing."""
    engine = create_async_engine(
        "sqlite+aiosqlite:///:memory:",
        echo=False,
        poolclass=StaticPool,
    )
    yield engine
    await engine.dispose()


@pytest.fixture
async def test_engine_with_tables(test_engine):
    """Create engine with tables already created."""
    async with test_engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    yield test_engine


# =============================================================================
# Database Initialization Tests
# =============================================================================


@pytest.mark.asyncio
async def test_initialize_database_success(test_engine):
    """Test successful database initialization."""
    result = await initialize_database(
        engine=test_engine,
        create_schema=True,
        validate_schema=True,
        seed_data=False,
    )
    
    assert result["success"] is True
    assert result["schema_version"] == CURRENT_SCHEMA_VERSION
    assert len(result["tables_created"]) == len(EXPECTED_TABLES)
    assert result["validation_result"]["valid"] is True
    assert result["health_check"]["healthy"] is True


@pytest.mark.asyncio
async def test_initialize_database_without_schema_creation(test_engine_with_tables):
    """Test initialization without creating schema."""
    result = await initialize_database(
        engine=test_engine_with_tables,
        create_schema=False,
        validate_schema=True,
        seed_data=False,
    )
    
    assert result["success"] is True
    assert result["schema_version"] == CURRENT_SCHEMA_VERSION
    assert result["tables_created"] == []
    assert result["validation_result"]["valid"] is True


@pytest.mark.asyncio
async def test_initialize_database_with_seeding(test_engine):
    """Test initialization with data seeding."""
    result = await initialize_database(
        engine=test_engine,
        create_schema=True,
        validate_schema=True,
        seed_data=True,
    )
    
    assert result["success"] is True
    # Seeding should complete without errors
    # (even if no actual data is seeded for empty database)


# =============================================================================
# Schema Creation Tests
# =============================================================================


@pytest.mark.asyncio
async def test_create_database_schema(test_engine):
    """Test creating database schema."""
    tables = await create_database_schema(test_engine)
    
    assert len(tables) == len(EXPECTED_TABLES)
    assert set(tables) == EXPECTED_TABLES


@pytest.mark.asyncio
async def test_create_database_schema_idempotent(test_engine_with_tables):
    """Test that creating schema is idempotent."""
    # Tables already exist
    tables = await create_database_schema(test_engine_with_tables)
    
    # Should return existing tables, not create duplicates
    assert len(tables) == len(EXPECTED_TABLES)
    assert set(tables) == EXPECTED_TABLES


@pytest.mark.asyncio
async def test_create_schema_uses_default_engine_when_none():
    """Test schema creation with None engine uses default."""
    with patch("src.server.database.init.get_engine") as mock_get_engine:
        # Create a real test engine
        test_engine = create_async_engine(
            "sqlite+aiosqlite:///:memory:",
            echo=False,
            poolclass=StaticPool,
        )
        mock_get_engine.return_value = test_engine
        
        # This should call get_engine() and work with test engine
        tables = await create_database_schema(engine=None)
        assert len(tables) == len(EXPECTED_TABLES)
        
        await test_engine.dispose()


# =============================================================================
# Schema Validation Tests
# =============================================================================


@pytest.mark.asyncio
async def test_validate_database_schema_valid(test_engine_with_tables):
    """Test validating a valid schema."""
    result = await validate_database_schema(test_engine_with_tables)
    
    assert result["valid"] is True
    assert len(result["missing_tables"]) == 0
    assert len(result["issues"]) == 0


@pytest.mark.asyncio
async def test_validate_database_schema_empty(test_engine):
    """Test validating an empty database."""
    result = await validate_database_schema(test_engine)
    
    assert result["valid"] is False
    assert len(result["missing_tables"]) == len(EXPECTED_TABLES)
    assert len(result["issues"]) > 0


@pytest.mark.asyncio
async def test_validate_database_schema_partial(test_engine):
    """Test validating partially created schema."""
    # Create only one table
    async with test_engine.begin() as conn:
        await conn.execute(
            text("""
                CREATE TABLE anime_series (
                    id INTEGER PRIMARY KEY,
                    key VARCHAR(255) UNIQUE NOT NULL,
                    name VARCHAR(500) NOT NULL
                )
            """)
        )
    
    result = await validate_database_schema(test_engine)
    
    assert result["valid"] is False
    assert len(result["missing_tables"]) == len(EXPECTED_TABLES) - 1
    assert "anime_series" not in result["missing_tables"]


# =============================================================================
# Schema Version Tests
# =============================================================================


@pytest.mark.asyncio
async def test_get_schema_version_empty(test_engine):
    """Test getting schema version from empty database."""
    version = await get_schema_version(test_engine)
    assert version == "empty"


@pytest.mark.asyncio
async def test_get_schema_version_current(test_engine_with_tables):
    """Test getting schema version from current schema."""
    version = await get_schema_version(test_engine_with_tables)
    assert version == CURRENT_SCHEMA_VERSION


@pytest.mark.asyncio
async def test_get_schema_version_unknown(test_engine):
    """Test getting schema version from unknown schema."""
    # Create some random tables
    async with test_engine.begin() as conn:
        await conn.execute(
            text("CREATE TABLE random_table (id INTEGER PRIMARY KEY)")
        )
    
    version = await get_schema_version(test_engine)
    assert version == "unknown"


# =============================================================================
# Data Seeding Tests
# =============================================================================


@pytest.mark.asyncio
async def test_seed_initial_data_empty_database(test_engine_with_tables):
    """Test seeding data into empty database."""
    # Should complete without errors
    await seed_initial_data(test_engine_with_tables)
    
    # Verify database is still empty (no sample data)
    async with test_engine_with_tables.connect() as conn:
        result = await conn.execute(text("SELECT COUNT(*) FROM anime_series"))
        count = result.scalar()
        assert count == 0


@pytest.mark.asyncio
async def test_seed_initial_data_existing_data(test_engine_with_tables):
    """Test seeding skips if data already exists."""
    # Add some data
    async with test_engine_with_tables.begin() as conn:
        await conn.execute(
            text("""
                INSERT INTO anime_series (key, name, site, folder)
                VALUES ('test-key', 'Test Anime', 'https://test.com', '/test')
            """)
        )
    
    # Seeding should skip
    await seed_initial_data(test_engine_with_tables)
    
    # Verify only one record exists
    async with test_engine_with_tables.connect() as conn:
        result = await conn.execute(text("SELECT COUNT(*) FROM anime_series"))
        count = result.scalar()
        assert count == 1


# =============================================================================
# Health Check Tests
# =============================================================================


@pytest.mark.asyncio
async def test_check_database_health_healthy(test_engine_with_tables):
    """Test health check on healthy database."""
    result = await check_database_health(test_engine_with_tables)
    
    assert result["healthy"] is True
    assert result["accessible"] is True
    assert result["tables"] == len(EXPECTED_TABLES)
    assert result["connectivity_ms"] > 0
    assert len(result["issues"]) == 0


@pytest.mark.asyncio
async def test_check_database_health_empty(test_engine):
    """Test health check on empty database."""
    result = await check_database_health(test_engine)
    
    assert result["healthy"] is False
    assert result["accessible"] is True
    assert result["tables"] == 0
    assert len(result["issues"]) > 0


@pytest.mark.asyncio
async def test_check_database_health_connection_error():
    """Test health check with connection error."""
    mock_engine = AsyncMock(spec=AsyncEngine)
    mock_engine.connect.side_effect = Exception("Connection failed")
    
    result = await check_database_health(mock_engine)
    
    assert result["healthy"] is False
    assert result["accessible"] is False
    assert len(result["issues"]) > 0
    assert "Connection failed" in result["issues"][0]


# =============================================================================
# Backup Tests
# =============================================================================


@pytest.mark.asyncio
async def test_create_database_backup_not_sqlite():
    """Test backup fails for non-SQLite databases."""
    with patch("src.server.database.init.settings") as mock_settings:
        mock_settings.database_url = "postgresql://localhost/test"
        
        with pytest.raises(NotImplementedError):
            await create_database_backup()


@pytest.mark.asyncio
async def test_create_database_backup_file_not_found():
    """Test backup fails if database file doesn't exist."""
    with patch("src.server.database.init.settings") as mock_settings:
        mock_settings.database_url = "sqlite:///nonexistent.db"
        
        with pytest.raises(RuntimeError, match="Database file not found"):
            await create_database_backup()


@pytest.mark.asyncio
async def test_create_database_backup_success(tmp_path):
    """Test successful database backup."""
    # Create a temporary database file
    db_file = tmp_path / "test.db"
    db_file.write_text("test data")
    
    backup_file = tmp_path / "backup.db"
    
    with patch("src.server.database.init.settings") as mock_settings:
        mock_settings.database_url = f"sqlite:///{db_file}"
        
        result = await create_database_backup(backup_path=backup_file)
        
        assert result == backup_file
        assert backup_file.exists()
        assert backup_file.read_text() == "test data"


# =============================================================================
# Utility Function Tests
# =============================================================================


def test_get_database_info():
    """Test getting database configuration info."""
    info = get_database_info()
    
    assert "database_url" in info
    assert "database_type" in info
    assert "schema_version" in info
    assert "expected_tables" in info
    assert info["schema_version"] == CURRENT_SCHEMA_VERSION
    assert set(info["expected_tables"]) == EXPECTED_TABLES


# =============================================================================
# Integration Tests
# =============================================================================


@pytest.mark.asyncio
async def test_full_initialization_workflow(test_engine):
    """Test complete initialization workflow."""
    # 1. Initialize database
    result = await initialize_database(
        engine=test_engine,
        create_schema=True,
        validate_schema=True,
        seed_data=True,
    )
    
    assert result["success"] is True
    
    # 2. Verify schema
    validation = await validate_database_schema(test_engine)
    assert validation["valid"] is True
    
    # 3. Check version
    version = await get_schema_version(test_engine)
    assert version == CURRENT_SCHEMA_VERSION
    
    # 4. Health check
    health = await check_database_health(test_engine)
    assert health["healthy"] is True
    assert health["accessible"] is True


@pytest.mark.asyncio
async def test_reinitialize_existing_database(test_engine_with_tables):
    """Test reinitializing an existing database."""
    # Should be idempotent - safe to call multiple times
    result1 = await initialize_database(
        engine=test_engine_with_tables,
        create_schema=True,
        validate_schema=True,
    )
    
    result2 = await initialize_database(
        engine=test_engine_with_tables,
        create_schema=True,
        validate_schema=True,
    )
    
    assert result1["success"] is True
    assert result2["success"] is True
    assert result1["schema_version"] == result2["schema_version"]


# =============================================================================
# Error Handling Tests
# =============================================================================


@pytest.mark.asyncio
async def test_initialize_database_with_creation_error():
    """Test initialization handles schema creation errors."""
    mock_engine = AsyncMock(spec=AsyncEngine)
    mock_engine.begin.side_effect = Exception("Creation failed")
    
    with pytest.raises(RuntimeError, match="Failed to initialize database"):
        await initialize_database(
            engine=mock_engine,
            create_schema=True,
        )


@pytest.mark.asyncio
async def test_create_schema_with_connection_error():
    """Test schema creation handles connection errors."""
    mock_engine = AsyncMock(spec=AsyncEngine)
    mock_engine.begin.side_effect = Exception("Connection failed")
    
    with pytest.raises(RuntimeError, match="Schema creation failed"):
        await create_database_schema(mock_engine)


@pytest.mark.asyncio
async def test_validate_schema_with_inspection_error():
    """Test validation handles inspection errors gracefully."""
    mock_engine = AsyncMock(spec=AsyncEngine)
    mock_engine.connect.side_effect = Exception("Inspection failed")
    
    result = await validate_database_schema(mock_engine)
    
    assert result["valid"] is False
    assert len(result["issues"]) > 0
    assert "Inspection failed" in result["issues"][0]


# =============================================================================
# Constants Tests
# =============================================================================


def test_schema_constants():
    """Test that schema constants are properly defined."""
    assert CURRENT_SCHEMA_VERSION == "1.0.1"
    assert len(EXPECTED_TABLES) == 5
    assert "anime_series" in EXPECTED_TABLES
    assert "episodes" in EXPECTED_TABLES
    assert "download_queue" in EXPECTED_TABLES
    assert "user_sessions" in EXPECTED_TABLES
    assert "system_settings" in EXPECTED_TABLES


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