"""Tests for AppCertManager TTL-based cert caching (relay.py)."""

from __future__ import annotations

import asyncio
import os
import time
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from cube_cloud.tunnel.relay import AppCertManager


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_cert_dir(tmp_path, app_name: str) -> str:
    """Create a fake cert directory with a tlscert file."""
    cert_dir = tmp_path / f"tbot-app-{app_name}"
    cert_dir.mkdir(parents=True, exist_ok=True)
    (cert_dir / "tlscert").write_text("fake-cert")
    (cert_dir / "key").write_text("fake-key")
    return str(cert_dir)


def _mock_generate_cert(cert_dir: str):
    """Return an AsyncMock for _generate_cert that returns cert_dir."""
    return AsyncMock(return_value=cert_dir)


# ---------------------------------------------------------------------------
# Cache hit — cert is fresh
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_cache_hit_returns_cached_dir(tmp_path):
    """A fresh cached cert should be returned without regeneration."""
    mgr = AppCertManager()
    cert_dir = _make_cert_dir(tmp_path, "myapp")

    # Pre-populate cache
    mgr._certs["myapp"] = cert_dir
    mgr._cert_times["myapp"] = time.time()  # just now

    with patch.object(mgr, "_generate_cert", new=AsyncMock()) as mock_gen:
        result = await mgr.get_cert_dir("myapp")

    assert result == cert_dir
    mock_gen.assert_not_called()


# ---------------------------------------------------------------------------
# Cache miss — first request
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_cache_miss_generates_cert(tmp_path):
    """First request for an app should call _generate_cert."""
    mgr = AppCertManager()
    cert_dir = _make_cert_dir(tmp_path, "newapp")

    with patch.object(mgr, "_generate_cert", new=AsyncMock(return_value=cert_dir)):
        result = await mgr.get_cert_dir("newapp")

    assert result == cert_dir
    assert mgr._certs["newapp"] == cert_dir
    assert mgr._cert_times["newapp"] > 0


# ---------------------------------------------------------------------------
# Cache expired — TTL exceeded
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_cache_expired_regenerates(tmp_path):
    """An expired cert should trigger regeneration."""
    mgr = AppCertManager()
    old_dir = _make_cert_dir(tmp_path, "oldapp")
    new_dir = _make_cert_dir(tmp_path, "oldapp-new")

    # Pre-populate with old timestamp (way past TTL)
    mgr._certs["oldapp"] = old_dir
    mgr._cert_times["oldapp"] = time.time() - (mgr.CERT_TTL_SECONDS + 100)

    with patch.object(mgr, "_generate_cert", new=AsyncMock(return_value=new_dir)):
        result = await mgr.get_cert_dir("oldapp")

    assert result == new_dir
    assert mgr._certs["oldapp"] == new_dir
    # New timestamp should be recent
    assert time.time() - mgr._cert_times["oldapp"] < 5


# ---------------------------------------------------------------------------
# Cache entry with missing tlscert file → regenerate
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_cache_missing_tlscert_regenerates(tmp_path):
    """If the cached cert_dir exists but tlscert is gone, regenerate."""
    mgr = AppCertManager()
    cert_dir = str(tmp_path / "tbot-app-broken")
    os.makedirs(cert_dir, exist_ok=True)
    # No tlscert file!

    mgr._certs["broken"] = cert_dir
    mgr._cert_times["broken"] = time.time()  # fresh time

    new_dir = _make_cert_dir(tmp_path, "broken-new")
    with patch.object(mgr, "_generate_cert", new=AsyncMock(return_value=new_dir)):
        result = await mgr.get_cert_dir("broken")

    assert result == new_dir


# ---------------------------------------------------------------------------
# Cache entry with deleted directory → regenerate
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_cache_deleted_dir_regenerates(tmp_path):
    """If the cached cert_dir no longer exists, regenerate."""
    mgr = AppCertManager()
    mgr._certs["gone"] = "/nonexistent/path"
    mgr._cert_times["gone"] = time.time()

    new_dir = _make_cert_dir(tmp_path, "gone-new")
    with patch.object(mgr, "_generate_cert", new=AsyncMock(return_value=new_dir)):
        result = await mgr.get_cert_dir("gone")

    assert result == new_dir


# ---------------------------------------------------------------------------
# Invalidate clears both cache and time
# ---------------------------------------------------------------------------


def test_invalidate_clears_cache(tmp_path):
    """invalidate() should remove both cert dir reference and timestamp."""
    mgr = AppCertManager()
    cert_dir = _make_cert_dir(tmp_path, "inv")
    mgr._certs["inv"] = cert_dir
    mgr._cert_times["inv"] = time.time()

    mgr.invalidate("inv")

    assert "inv" not in mgr._certs
    assert "inv" not in mgr._cert_times


def test_invalidate_nonexistent_is_noop():
    """invalidate() on unknown app should not raise."""
    mgr = AppCertManager()
    mgr.invalidate("unknown")  # should not raise


# ---------------------------------------------------------------------------
# TTL constant is sane
# ---------------------------------------------------------------------------


def test_cert_ttl_value():
    """CERT_TTL_SECONDS should be 23 hours (82800 seconds)."""
    assert AppCertManager.CERT_TTL_SECONDS == 23 * 3600
    assert AppCertManager.CERT_TTL_SECONDS == 82800
