"""Tests for the fail2ban metadata service."""

from __future__ import annotations

from unittest.mock import AsyncMock, patch

from app.services.fail2ban_metadata_service import Fail2BanMetadataService
from app.exceptions import Fail2BanConnectionError


async def test_get_db_path_caches_result() -> None:
    """Same socket path should be resolved only once and reused from cache."""
    service = Fail2BanMetadataService()
    client = AsyncMock()
    client.send = AsyncMock(return_value=(0, "/tmp/fail2ban.sqlite3"))
    client.__aenter__.return_value = client
    client.__aexit__.return_value = None

    with patch(
        "app.services.fail2ban_metadata_service.Fail2BanClient",
        return_value=client,
    ) as mock_client_cls:
        result1 = await service.get_db_path("/tmp/fail2ban.sock")
        result2 = await service.get_db_path("/tmp/fail2ban.sock")

    assert result1 == "/tmp/fail2ban.sqlite3"
    assert result2 == "/tmp/fail2ban.sqlite3"
    assert mock_client_cls.call_count == 1
    assert client.send.call_count == 1


async def test_get_db_path_uses_cached_path_when_refresh_fails() -> None:
    """When explicit refresh fails, the previously cached path is returned."""
    service = Fail2BanMetadataService()
    first_client = AsyncMock()
    first_client.send = AsyncMock(return_value=(0, "/tmp/fail2ban.sqlite3"))
    first_client.__aenter__.return_value = first_client
    first_client.__aexit__.return_value = None

    second_client = AsyncMock()
    second_client.send = AsyncMock(side_effect=Fail2BanConnectionError("socket down", "/tmp/fail2ban.sock"))
    second_client.__aenter__.return_value = second_client
    second_client.__aexit__.return_value = None

    with patch(
        "app.services.fail2ban_metadata_service.Fail2BanClient",
        side_effect=[first_client, second_client],
    ) as mock_client_cls:
        initial_path = await service.get_db_path("/tmp/fail2ban.sock")
        refreshed_path = await service.get_db_path(
            "/tmp/fail2ban.sock",
            force_refresh=True,
        )

    assert initial_path == "/tmp/fail2ban.sqlite3"
    assert refreshed_path == "/tmp/fail2ban.sqlite3"
    assert mock_client_cls.call_count == 2
    assert second_client.send.call_count == 1


async def test_invalidate_db_path_forces_re_resolution() -> None:
    """Invalidating the cache forces a new metadata resolution."""
    service = Fail2BanMetadataService()
    first_client = AsyncMock()
    first_client.send = AsyncMock(return_value=(0, "/tmp/fail2ban-v1.sqlite3"))
    first_client.__aenter__.return_value = first_client
    first_client.__aexit__.return_value = None

    second_client = AsyncMock()
    second_client.send = AsyncMock(return_value=(0, "/tmp/fail2ban-v2.sqlite3"))
    second_client.__aenter__.return_value = second_client
    second_client.__aexit__.return_value = None

    with patch(
        "app.services.fail2ban_metadata_service.Fail2BanClient",
        side_effect=[first_client, second_client],
    ) as mock_client_cls:
        first_path = await service.get_db_path("/tmp/fail2ban.sock")
        service.invalidate_db_path("/tmp/fail2ban.sock")
        second_path = await service.get_db_path("/tmp/fail2ban.sock")

    assert first_path == "/tmp/fail2ban-v1.sqlite3"
    assert second_path == "/tmp/fail2ban-v2.sqlite3"
    assert mock_client_cls.call_count == 2
