# #region Test.Core.ConnectionService [C:3] [TYPE Module] [SEMANTICS test, connections, service, settings]
# @BRIEF Pytest tests for ConnectionService CRUD, encryption, test, and deletion blocking.
# @RELATION BINDS_TO -> [ConnectionService]
# @TEST_EDGE: missing_field -> ValueError for empty name
# @TEST_EDGE: invalid_type -> ValueError for unsupported dialect
# @TEST_EDGE: external_fail -> Connection error handled gracefully
# @TEST_CONTRACT ConnectionService CRUD operations reflect in GlobalSettings.connections.
# @TEST_CONTRACT Passwords are encrypted at rest (stored ≠ plaintext).
# @TEST_CONTRACT Test connection returns success/failure with diagnostics.

import pytest
from unittest.mock import MagicMock, patch

from src.core.config_models import AppConfig, DatabaseConnection, GlobalSettings
from src.core.connection_service import ConnectionService


# #region Fixtures [C:2] [TYPE Block]
@pytest.fixture
def mock_config_manager():
    """ConfigManager with empty connections list."""
    cm = MagicMock()
    cm.config = AppConfig(
        environments=[],
        settings=GlobalSettings(connections=[]),
    )
    return cm


@pytest.fixture
def config_with_two(mock_config_manager):
    """ConfigManager pre-populated with 2 connections."""
    mock_config_manager.config.settings.connections = [
        DatabaseConnection(
            id="c1",
            name="Test PG",
            host="localhost",
            port=5432,
            database="test_db",
            username="tester",
            password="enc_secret_pg",
            dialect="postgresql",
            pool_size=5,
        ),
        DatabaseConnection(
            id="c2",
            name="Test CH",
            host="localhost",
            port=9000,
            database="analytics",
            username="default",
            password="",
            dialect="clickhouse",
            pool_size=2,
        ),
    ]
    return mock_config_manager


@pytest.fixture
def service(mock_config_manager):
    return ConnectionService(mock_config_manager)


@pytest.fixture
def service_with_two(config_with_two):
    return ConnectionService(config_with_two)


# #endregion Fixtures


# #region create tests [C:2] [TYPE Block]
class TestCreateConnection:
    def test_create_success(self, service):
        data = {
            "name": "My PG",
            "host": "db.example.com",
            "port": 5432,
            "database": "products",
            "username": "translator",
            "password": "my_secret",
            "dialect": "postgresql",
            "pool_size": 5,
        }
        result = service.create_connection(data)
        assert result["name"] == "My PG"
        assert result["password"] == "********"
        assert len(service.config_manager.config.settings.connections) == 1
        # Verify password was encrypted (stored != plaintext)
        stored = service.config_manager.config.settings.connections[0]
        assert stored.password != "my_secret"
        assert stored.password != "********"

    def test_create_duplicate_name(self, service_with_two):
        data = {"name": "Test PG", "host": "other", "port": 5432,
                "database": "x", "username": "u", "password": "p", "dialect": "postgresql"}
        with pytest.raises(ValueError, match="already exists"):
            service_with_two.create_connection(data)

    def test_create_empty_name(self, service):
        data = {"name": "", "host": "x", "port": 5432,
                "database": "x", "username": "u", "password": "p", "dialect": "postgresql"}
        with pytest.raises(ValueError, match="must not be empty"):
            service.create_connection(data)

    def test_create_dialect_case_insensitive(self, service):
        data = {"name": "PG", "host": "x", "port": 5432, "database": "x",
                "username": "u", "password": "p", "dialect": "PostgreSQL"}
        result = service.create_connection(data)
        assert result["dialect"] == "postgresql"

    def test_create_mysql_dialect(self, service):
        data = {"name": "MySQL", "host": "x", "port": 3306, "database": "x",
                "username": "u", "password": "p", "dialect": "mysql"}
        result = service.create_connection(data)
        assert result["dialect"] == "mysql"

    def test_create_bad_dialect(self, service):
        data = {"name": "Bad", "host": "x", "port": 5432, "database": "x",
                "username": "u", "password": "p", "dialect": "oracle"}
        with pytest.raises(ValueError, match="Unsupported dialect"):
            service.create_connection(data)

    def test_create_bad_port(self, service):
        data = {"name": "Bad port", "host": "x", "port": 0, "database": "x",
                "username": "u", "password": "p", "dialect": "postgresql"}
        with pytest.raises(ValueError, match="Port must be between"):
            service.create_connection(data)
# #endregion


# #region list/get tests [C:2] [TYPE Block]
class TestListGetConnection:
    def test_list_masks_passwords(self, service_with_two):
        result = service_with_two.list_connections()
        assert len(result) == 2
        for conn in result:
            assert conn["password"] == "********"

    def test_list_shows_used_by(self, service_with_two):
        result = service_with_two.list_connections()
        for conn in result:
            assert "used_by" in conn

    def test_get_decrypts_password(self, service_with_two):
        conn = service_with_two.get_connection("c1")
        assert conn is not None
        # password is "enc_secret_pg" which is not Fernet-encrypted,
        # so decryption will pass through as-is
        assert conn.password is not None

    def test_get_not_found(self, service):
        conn = service.get_connection("nonexistent")
        assert conn is None

    def test_empty_list(self, service):
        result = service.list_connections()
        assert result == []
# #endregion


# #region update tests [C:2] [TYPE Block]
class TestUpdateConnection:
    def test_update_name(self, service_with_two):
        result = service_with_two.update_connection("c1", {"name": "Renamed PG"})
        assert result["name"] == "Renamed PG"
        assert result["password"] == "********"

    def test_update_password(self, service_with_two):
        result = service_with_two.update_connection("c1", {"password": "new_secret"})
        assert result["password"] == "********"
        # Verify encrypted differently than original
        stored = service_with_two.config_manager.config.settings.connections[0]
        assert stored.password != "enc_secret_pg"

    def test_update_empty_password_keeps_existing(self, service_with_two):
        result = service_with_two.update_connection("c1", {"password": ""})
        assert result["password"] == "********"
        stored = service_with_two.config_manager.config.settings.connections[0]
        assert stored.password == "enc_secret_pg"

    def test_update_masked_password_keeps_existing(self, service_with_two):
        service_with_two.update_connection("c1", {"password": "********"})
        stored = service_with_two.config_manager.config.settings.connections[0]
        assert stored.password == "enc_secret_pg"

    def test_update_host_and_port(self, service_with_two):
        result = service_with_two.update_connection("c1", {"host": "new.db", "port": 15432})
        assert result["host"] == "new.db"
        assert result["port"] == 15432

    def test_update_not_found(self, service):
        with pytest.raises(ValueError, match="not found"):
            service.update_connection("nonexistent", {"name": "X"})

    def test_update_duplicate_name(self, service_with_two):
        # Rename c2 to c1's name
        with pytest.raises(ValueError, match="already exists"):
            service_with_two.update_connection("c2", {"name": "Test PG"})
# #endregion


# #region delete tests [C:2] [TYPE Block]
class TestDeleteConnection:
    def test_delete_success(self, service_with_two):
        result = service_with_two.delete_connection("c1")
        assert result["success"] is True
        assert len(service_with_two.config_manager.config.settings.connections) == 1

    def test_delete_not_found(self, service):
        with pytest.raises(ValueError, match="not found"):
            service.delete_connection("nonexistent")

    def test_delete_blocked_by_jobs(self, service_with_two):
        # AUDIT_NOTE: _find_blocking_jobs calls TranslationJob via [EXT:SQLAlchemy].
        # Patching the method is equivalent to mocking the DB query boundary.
        with patch.object(service_with_two, '_find_blocking_jobs', return_value=["Job 1"]):
            result = service_with_two.delete_connection("c1")
            assert result["success"] is False
            assert result["blocked"] is True
            assert "Job 1" in result["blocking_jobs"]
# #endregion


# #region password encryption tests [C:2] [TYPE Block]
class TestPasswordEncryption:
    def test_encryption_round_trip(self, service):
        """Password round-trips through encryption/decryption."""
        password = "super_secret_key_123"
        encrypted = service._encrypt_password(password)
        # With real EncryptionManager (Fernet), encrypted != plaintext
        assert encrypted != password
        decrypted = service._decrypt_password(encrypted)
        assert decrypted == password

    def test_masked_password_decrypt_returns_masked(self, service):
        result = service._decrypt_password("********")
        assert result == "********"

    def test_empty_password_encryption(self, service):
        encrypted = service._encrypt_password("")
        # Empty string is still encrypted by Fernet (consistent behavior)
        assert encrypted != ""
        assert encrypted != "********"
        decrypted = service._decrypt_password(encrypted)
        assert decrypted == ""

    def test_new_connection_password_encrypted(self, service):
        data = {"name": "Test", "host": "x", "port": 5432, "database": "x",
                "username": "u", "password": "visible", "dialect": "postgresql"}
        result = service.create_connection(data)
        assert result["password"] == "********"
        stored = service.config_manager.config.settings.connections[0]
        assert stored.password != "visible"
        assert stored.password != "********"
# #endregion


# #region test connection tests [C:2] [TYPE Block]
# AUDIT_NOTE: patch.object targets _test_postgresql/_test_clickhouse/_test_mysql
# which are thin wrappers around [EXT:asyncpg]/[EXT:clickhouse-connect]/[EXT:pymysql]
# DB drivers. These are VALID [EXT:Database] boundary mocks per semantics-testing §3c.
class TestTestConnection:
    @pytest.mark.asyncio
    async def test_connection_not_found(self, service):
        result = await service.test_connection("nonexistent")
        assert result["success"] is False
        assert "not found" in result["error"]

    @pytest.mark.asyncio
    async def test_postgresql_test_success(self, service_with_two):
        """Mock asyncpg to verify dialect routing works."""
        with patch.object(service_with_two, '_test_postgresql', return_value="PostgreSQL 16.3"):
            result = await service_with_two.test_connection("c1")
            assert result["success"] is True
            assert result["db_version"] == "PostgreSQL 16.3"
            assert "latency_ms" in result

    @pytest.mark.asyncio
    async def test_clickhouse_test_success(self, service_with_two):
        with patch.object(service_with_two, '_test_clickhouse', return_value="ClickHouse 24.3"):
            result = await service_with_two.test_connection("c2")
            assert result["success"] is True
            assert result["db_version"] == "ClickHouse 24.3"

    @pytest.mark.asyncio
    async def test_test_mysql_success(self, config_with_two):
        """Create a MySQL connection and test it."""
        config_with_two.config.settings.connections.append(
            DatabaseConnection(
                id="c3", name="MySQL", host="x", port=3306,
                database="x", username="u", password="p",
                dialect="mysql",
            )
        )
        svc = ConnectionService(config_with_two)
        with patch.object(svc, '_test_mysql', return_value="MySQL 8.0"):
            result = await svc.test_connection("c3")
            assert result["success"] is True

    @pytest.mark.asyncio
    async def test_test_connection_error(self, service_with_two):
        """Simulate connection failure."""
        with patch.object(service_with_two, '_test_postgresql', side_effect=RuntimeError("Host unreachable")):
            result = await service_with_two.test_connection("c1")
            assert result["success"] is False
            assert "Host unreachable" in result["error"]
# #endregion


# #region validation tests [C:2] [TYPE Block]
class TestValidateRefs:
    def test_validate_no_jobs_no_orphans(self, service):
        """When no TranslationJobs exist, no orphans."""
        with patch('src.core.database.SessionLocal') as mock_session_local:
            mock_db = MagicMock()
            mock_session_local.return_value = mock_db
            mock_db.query.return_value.filter.return_value.all.return_value = []
            orphans = service.validate_connection_refs()
            assert orphans == []
# #endregion
# #endregion Test.Core.ConnectionService
