"""Comprehensive tests for the refactored sangreal-db core functionality."""
import pytest
import pandas as pd
from unittest.mock import Mock, MagicMock, patch
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String
from sqlalchemy.orm import sessionmaker
from sqlalchemy.exc import InvalidRequestError, NoSuchTableError

from sangreal_db.core.database import DataBase
from sangreal_db.core.dialects import DialectFactory, MySQLInsertStrategy
from sangreal_db.core.exceptions import TableNotFoundError, PrimaryKeyError, ValidationError
from sangreal_db.core.table_registry import TableRegistry


class TestDialectFactory:
    """Test the dialect factory and insert strategies."""

    def test_get_mysql_strategy(self):
        strategy = DialectFactory.get_strategy('mysql')
        assert isinstance(strategy, MySQLInsertStrategy)
        assert strategy.get_ignore_clause(True) == 'IGNORE'
        assert strategy.get_ignore_clause(False) == ''

    def test_get_sqlite_strategy(self):
        strategy = DialectFactory.get_strategy('sqlite')
        assert strategy.get_ignore_clause(True) == 'OR REPLACE'
        assert strategy.get_ignore_clause(False) == ''

    def test_get_unknown_strategy(self):
        strategy = DialectFactory.get_strategy('unknown_db')
        # Should return DefaultInsertStrategy for unknown dialects
        with pytest.raises(Exception):
            strategy.validate_bulk_insert(True)

    def test_case_insensitive_dialect_names(self):
        mysql1 = DialectFactory.get_strategy('mysql')
        mysql2 = DialectFactory.get_strategy('MySQL')
        mysql3 = DialectFactory.get_strategy('MYSQL')
        assert type(mysql1) == type(mysql2) == type(mysql3)


class TestTableRegistry:
    """Test the table registry functionality."""

    @pytest.fixture
    def mock_engine(self):
        engine = Mock()
        engine.dialect.name = 'sqlite'
        return engine

    @pytest.fixture
    def mock_inspector(self, mock_engine):
        insp = Mock()
        insp.get_table_names.return_value = ['users', 'products', 'orders']
        return insp

    @patch('sangreal_db.core.table_registry.reflection.Inspector.from_engine')
    def test_load_table_names(self, mock_from_engine, mock_engine, mock_inspector):
        mock_from_engine.return_value = mock_inspector
        registry = TableRegistry(mock_engine)

        assert registry.available_tables == ['users', 'products', 'orders']
        assert registry.table_exists('users')
        assert registry.table_exists('USERS')  # Case sensitive check
        assert not registry.table_exists('nonexistent')

    @patch('sangreal_db.core.table_registry.reflection.Inspector.from_engine')
    def test_get_table_variants(self, mock_from_engine, mock_engine, mock_inspector):
        mock_from_engine.return_value = mock_inspector
        registry = TableRegistry(mock_engine)

        variants = registry.get_table_variants('TableName')
        assert set(variants) == {'TableName', 'tablename', 'TABLENAME'}

        # Test with lowercase
        variants = registry.get_table_variants('lowercase')
        assert set(variants) == {'lowercase', 'LOWERCASE'}

    @patch('sangreal_db.core.table_registry.reflection.Inspector.from_engine')
    def test_table_not_found_error(self, mock_from_engine, mock_engine, mock_inspector):
        mock_from_engine.return_value = mock_inspector
        registry = TableRegistry(mock_engine)

        with pytest.raises(TableNotFoundError):
            registry._reflect_table('nonexistent_table')


class TestDataBaseRefactored:
    """Test the refactored DataBase class."""

    @pytest.fixture
    def mock_engine(self):
        engine = Mock()
        engine.dialect.name = 'sqlite'
        return engine

    @pytest.fixture
    def mock_session(self):
        session = Mock()
        return session

    def test_unbound_initialization(self):
        """Test initialization without bind."""
        db = DataBase(None)
        assert db._bind is None
        assert db._session is None
        assert db._table_registry is None

    @patch('sangreal_db.core.database.create_engine')
    def test_string_bind_initialization(self, mock_create_engine, mock_session):
        """Test initialization with string connection."""
        mock_engine = Mock()
        mock_create_engine.return_value = mock_engine

        with patch('sangreal_db.orm.SangrealSession', return_value=mock_session):
            with patch('sangreal_db.core.table_registry.TableRegistry'):
                db = DataBase('sqlite:///test.db')

                mock_create_engine.assert_called_once_with('sqlite:///test.db')
                assert db._bind == mock_engine

    def test_engine_bind_initialization(self, mock_engine, mock_session):
        """Test initialization with SQLAlchemy engine."""
        with patch('sangreal_db.orm.SangrealSession', return_value=mock_session):
            with patch('sangreal_db.core.table_registry.TableRegistry'):
                db = DataBase(mock_engine)
                assert db._bind == mock_engine

    @patch('sangreal_db.core.database.create_engine')
    def test_invalid_connection_string(self, mock_create_engine):
        """Test error handling for invalid connection strings."""
        mock_create_engine.side_effect = Exception("Invalid connection")

        with pytest.raises(ValidationError, match="Failed to create database engine"):
            DataBase('invalid://connection')

    def test_schema_normalization(self, mock_engine, mock_session):
        """Test schema parameter normalization."""
        with patch('sangreal_db.orm.SangrealSession', return_value=mock_session):
            with patch('sangreal_db.core.table_registry.TableRegistry'):
                db = DataBase(mock_engine, schema='None')
                assert db._schema is None

    def test_inject_method(self, mock_engine):
        """Test the inject method for reinitialization."""
        with patch.object(DataBase, '__init__') as mock_init:
            db = DataBase(None)
            db.inject(mock_engine, 'test_schema')
            mock_init.assert_called_once_with(mock_engine, 'test_schema')

    @patch('sangreal_db.core.table_registry.TableRegistry')
    @patch('sangreal_db.orm.SangrealSession')
    @patch('sangreal_db.core.database.DialectFactory')
    def test_setup_complete_initialization(self, mock_dialect_factory, mock_session_class,
                                        mock_registry_class, mock_engine):
        """Test complete initialization path."""
        mock_session = Mock()
        mock_session_class.return_value = mock_session
        mock_registry = Mock()
        mock_registry.available_tables = ['users', 'products']
        mock_registry_class.return_value = mock_registry
        mock_strategy = Mock()
        mock_dialect_factory.get_strategy.return_value = mock_strategy

        db = DataBase(mock_engine)

        # Verify all components are initialized
        assert db._bind == mock_engine
        assert db._session == mock_session
        assert db._table_registry == mock_registry
        assert db._insert_strategy == mock_strategy
        mock_dialect_factory.get_strategy.assert_called_once_with(mock_engine.dialect.name)

    def test_runtime_errors_when_not_connected(self):
        """Test that operations raise RuntimeError when not connected."""
        db = DataBase(None)

        with pytest.raises(RuntimeError, match="Database not connected"):
            db.query()

        with pytest.raises(RuntimeError, match="Database not connected"):
            db.update(None)

        with pytest.raises(RuntimeError, match="Database not connected"):
            db.insert(None, [])

        with pytest.raises(RuntimeError, match="Database not connected"):
            db.delete(None)

        with pytest.raises(RuntimeError, match="Database not connected"):
            db.commit()

    @patch('sangreal_db.core.table_registry.TableRegistry')
    @patch('sangreal_db.orm.SangrealSession')
    def test_table_access_lazy_loading(self, mock_session_class, mock_registry_class, mock_engine):
        """Test lazy table access functionality."""
        mock_session = Mock()
        mock_session_class.return_value = mock_session
        mock_registry = Mock()
        mock_registry.available_tables = ['users']
        mock_registry.table_exists.return_value = True
        mock_table_class = Mock()
        mock_registry.get_table.return_value = mock_table_class
        mock_registry_class.return_value = mock_registry

        db = DataBase(mock_engine)

        # Test lazy loading
        result = db.users
        mock_registry.get_table.assert_called_with('users')
        assert result == mock_table_class

    @patch('sangreal_db.core.table_registry.TableRegistry')
    @patch('sangreal_db.orm.SangrealSession')
    def test_case_insensitive_table_access(self, mock_session_class, mock_registry_class, mock_engine):
        """Test case-insensitive table access."""
        mock_session = Mock()
        mock_session_class.return_value = mock_session
        mock_registry = Mock()
        mock_registry.available_tables = ['users']
        mock_registry.table_exists.side_effect = lambda name: name.lower() in ['users']
        mock_table_class = Mock()
        mock_registry.get_table.return_value = mock_table_class
        mock_registry_class.return_value = mock_registry

        db = DataBase(mock_engine)

        # Test different case variants
        assert db.users == mock_table_class
        assert db.USERS == mock_table_class
        assert db.Users == mock_table_class

    def test_attribute_error_for_nonexistent_tables(self, mock_engine):
        """Test AttributeError for nonexistent tables."""
        with patch('sangreal_db.orm.SangrealSession'):
            with patch('sangreal_db.core.table_registry.TableRegistry') as mock_registry_class:
                mock_registry = Mock()
                mock_registry.available_tables = ['users', 'products']
                mock_registry.table_exists.return_value = False
                mock_registry_class.return_value = mock_registry

                db = DataBase(mock_engine)

                with pytest.raises(AttributeError, match="'nonexistent' is not a valid table name"):
                    db.nonexistent

    def test_query_method(self, mock_engine):
        """Test the query method."""
        with patch('sangreal_db.orm.SangrealSession') as mock_session_class:
            mock_session = Mock()
            mock_query = Mock()
            mock_session.query.return_value = mock_query
            mock_session_class.return_value = mock_session

            with patch('sangreal_db.core.table_registry.TableRegistry'):
                db = DataBase(mock_engine)
                result = db.query('table1', 'table2')
                mock_session.query.assert_called_once_with('table1', 'table2')
                assert result == mock_query

    def test_update_method_single_object(self, mock_engine):
        """Test update method with single object."""
        with patch('sangreal_db.orm.SangrealSession') as mock_session_class:
            mock_session = Mock()
            mock_session_class.return_value = mock_session

            with patch('sangreal_db.core.table_registry.TableRegistry'):
                db = DataBase(mock_engine)
                test_obj = Mock()

                db.update(test_obj)
                mock_session.add.assert_called_once_with(test_obj)

    def test_update_method_multiple_objects(self, mock_engine):
        """Test update method with multiple objects."""
        with patch('sangreal_db.orm.SangrealSession') as mock_session_class:
            mock_session = Mock()
            mock_session_class.return_value = mock_session

            with patch('sangreal_db.core.table_registry.TableRegistry'):
                db = DataBase(mock_engine)
                test_objs = [Mock(), Mock(), Mock()]

                db.update(test_objs)
                mock_session.add_all.assert_called_once_with(test_objs)

    def test_insert_method_with_dataframe(self, mock_engine):
        """Test insert method with DataFrame input."""
        df = pd.DataFrame({'name': ['Alice', 'Bob'], 'age': [25, 30]})

        with patch('sangreal_db.orm.SangrealSession') as mock_session_class:
            mock_session = Mock()
            mock_execute_result = Mock()
            mock_session.execute.return_value = mock_execute_result
            mock_session_class.return_value = mock_session

            with patch('sangreal_db.core.table_registry.TableRegistry'):
                mock_strategy = Mock()
                mock_strategy.get_ignore_clause.return_value = ''
                mock_strategy.validate_bulk_insert.return_value = None

                with patch('sangreal_db.core.database.DialectFactory') as mock_factory:
                    mock_factory.get_strategy.return_value = mock_strategy

                    db = DataBase(mock_engine)
                    mock_table = Mock()
                    mock_table.__table__.insert.return_value.prefix_with.return_value.values.return_value = "INSERT stmt"

                    result = db.insert(mock_table, df)

                    # Verify DataFrame was converted to records
                    expected_data = df.to_dict('records')
                    mock_session.execute.assert_called_once()
                    assert result == mock_execute_result

    def test_insert_method_empty_dataframe(self, mock_engine):
        """Test insert method with empty DataFrame."""
        df = pd.DataFrame()

        with patch('sangreal_db.orm.SangrealSession'):
            with patch('sangreal_db.core.table_registry.TableRegistry'):
                db = DataBase(mock_engine)

                with pytest.raises(ValidationError, match="The input DataFrame is empty"):
                    db.insert(Mock(), df)

    def test_insert_method_invalid_input_type(self, mock_engine):
        """Test insert method with invalid input type."""
        with patch('sangreal_db.orm.SangrealSession'):
            with patch('sangreal_db.core.table_registry.TableRegistry'):
                db = DataBase(mock_engine)

                with pytest.raises(ValidationError, match="must be a pandas DataFrame or list of dicts"):
                    db.insert(Mock(), "invalid_input")

    def test_properties(self, mock_engine):
        """Test property accessors."""
        with patch('sangreal_db.orm.SangrealSession'):
            with patch('sangreal_db.core.table_registry.TableRegistry') as mock_registry_class:
                mock_registry = Mock()
                mock_registry.available_tables = ['users', 'products']
                mock_registry_class.return_value = mock_registry

                db = DataBase(mock_engine, schema='test_schema')

                assert db.bind == mock_engine
                assert db.schema == 'test_schema'
                assert db.tables == ['users', 'products']

    def test_repr(self):
        """Test __repr__ method."""
        db = DataBase(None)
        assert '<DataBase (unbound)>' in repr(db)

    def test_merge_method(self, mock_engine):
        """Test merge method."""
        with patch('sangreal_db.orm.SangrealSession') as mock_session_class:
            mock_session = Mock()
            mock_session.merge.return_value = "merged_object"
            mock_session_class.return_value = mock_session

            with patch('sangreal_db.core.table_registry.TableRegistry'):
                db = DataBase(mock_engine)

                # Test single object
                test_obj = Mock()
                result = db.merge(test_obj)
                mock_session.merge.assert_called_with(test_obj)
                assert result == "merged_object"

    def test_transaction_methods(self, mock_engine):
        """Test transaction management methods."""
        with patch('sangreal_db.orm.SangrealSession') as mock_session_class:
            mock_session = Mock()
            mock_session_class.return_value = mock_session

            with patch('sangreal_db.core.table_registry.TableRegistry'):
                db = DataBase(mock_engine)

                db.commit()
                mock_session.commit.assert_called_once()

                db.rollback()
                mock_session.rollback.assert_called_once()

                db.flush()
                mock_session.flush.assert_called_once_with(objects=None)

                db.flush(objects=[Mock()])
                mock_session.flush.assert_called_once_with(objects=[Mock()])


class TestDatabaseIntegration:
    """Integration tests for database operations."""

    @pytest.fixture
    def in_memory_db(self):
        """Create an in-memory SQLite database for testing."""
        engine = create_engine('sqlite:///:memory:')

        # Create test tables
        metadata = MetaData()
        users_table = Table(
            'users', metadata,
            Column('id', Integer, primary_key=True),
            Column('name', String(50)),
            Column('email', String(100))
        )
        metadata.create_all(engine)

        return engine, users_table

    def test_real_database_operations(self, in_memory_db):
        """Test real database operations with the refactored code."""
        engine, users_table = in_memory_db

        # Create DataBase instance
        db = DataBase(engine)

        # Test that tables are available
        assert 'users' in db.tables

        # Test table access
        users_class = db.users
        assert users_class is not None

        # Test query operations
        query = db.query(users_class)
        assert query is not None

        # Test insert operations
        test_data = [
            {'name': 'Alice', 'email': 'alice@test.com'},
            {'name': 'Bob', 'email': 'bob@test.com'}
        ]

        result = db.insert(users_class, test_data)
        assert result is not None

        db.commit()

        # Test query results
        inserted_users = db.query(users_class).all()
        assert len(inserted_users) == 2

    def test_dataframe_insertion(self, in_memory_db):
        """Test inserting DataFrame data."""
        engine, users_table = in_memory_db

        db = DataBase(engine)
        users_class = db.users

        # Create test DataFrame
        df = pd.DataFrame({
            'name': ['Charlie', 'Diana'],
            'email': ['charlie@test.com', 'diana@test.com']
        })

        # Insert DataFrame
        result = db.insert(users_class, df)
        db.commit()

        # Verify insertion
        inserted_users = db.query(users_class).all()
        assert len(inserted_users) == 2
        assert set(user.name for user in inserted_users) == {'Charlie', 'Diana'}

    def test_lazy_table_reflection(self, in_memory_db):
        """Test that table reflection works lazily."""
        engine, users_table = in_memory_db

        db = DataBase(engine)

        # Initially tables should be set up for lazy loading
        assert hasattr(db, 'users')

        # Access should trigger reflection
        users_class = db.users
        assert users_class is not None

        # Multiple accesses should return the same class
        assert db.users == users_class
        assert db.USERS == users_class  # Case variant


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