"""Tests for gaol log aggregation module."""

from __future__ import annotations

from datetime import datetime, timedelta
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import MagicMock, patch

import pytest

from gaol.logs import (
    LogCollector,
    LogConfig,
    LogEntry,
    LogLevel,
    LogQuery,
    LogSearcher,
    LogSource,
    LogStats,
    LogStore,
    MultiRuntimeCollector,
    RetentionPolicy,
    create_query,
    parse_log_level,
)


class TestLogModels:
    """Tests for log data models."""

    def test_log_entry_creation(self) -> None:
        """Test creating a log entry."""
        entry = LogEntry(
            container_id="abc123",
            container_name="test-container",
            runtime="docker",
            message="Hello, world!",
        )
        assert entry.container_name == "test-container"
        assert entry.message == "Hello, world!"
        assert entry.level == LogLevel.UNKNOWN
        assert entry.timestamp is not None

    def test_log_entry_with_level(self) -> None:
        """Test log entry with specified level."""
        entry = LogEntry(
            container_id="abc123",
            container_name="test",
            runtime="docker",
            message="Error occurred",
            level=LogLevel.ERROR,
        )
        assert entry.level == LogLevel.ERROR

    def test_log_entry_to_jsonl(self) -> None:
        """Test serializing entry to JSONL."""
        entry = LogEntry(
            container_id="abc",
            container_name="test",
            runtime="docker",
            message="Test message",
        )
        jsonl = entry.to_jsonl()
        assert "test" in jsonl
        assert "Test message" in jsonl

    def test_log_entry_from_jsonl(self) -> None:
        """Test deserializing entry from JSONL."""
        entry = LogEntry(
            container_id="abc",
            container_name="test",
            runtime="docker",
            message="Test message",
            level=LogLevel.INFO,
        )
        jsonl = entry.to_jsonl()
        restored = LogEntry.from_jsonl(jsonl)
        assert restored.container_name == entry.container_name
        assert restored.message == entry.message
        assert restored.level == entry.level

    def test_log_query_creation(self) -> None:
        """Test creating a log query."""
        query = LogQuery(
            since=datetime.now() - timedelta(hours=1),
            levels=[LogLevel.ERROR, LogLevel.WARNING],
            pattern="error",
        )
        assert query.pattern == "error"
        assert len(query.levels) == 2

    def test_log_query_last_n_hours(self) -> None:
        """Test creating a query for last N hours."""
        query = LogQuery.last_n_hours(2)
        assert query.since is not None
        assert query.since < datetime.now()
        assert query.since > datetime.now() - timedelta(hours=3)

    def test_log_query_errors_only(self) -> None:
        """Test creating an errors-only query."""
        query = LogQuery.errors_only()
        assert LogLevel.ERROR in query.levels
        assert LogLevel.CRITICAL in query.levels
        assert LogLevel.INFO not in query.levels

    def test_log_entry_matches_query_basic(self) -> None:
        """Test basic query matching."""
        entry = LogEntry(
            container_id="abc",
            container_name="test",
            runtime="docker",
            message="An error occurred",
            level=LogLevel.ERROR,
        )

        # Should match error query
        query = LogQuery(levels=[LogLevel.ERROR])
        assert entry.matches_query(query)

        # Should not match info query
        query = LogQuery(levels=[LogLevel.INFO])
        assert not entry.matches_query(query)

    def test_log_entry_matches_query_pattern(self) -> None:
        """Test pattern matching in query."""
        entry = LogEntry(
            container_id="abc",
            container_name="test",
            runtime="docker",
            message="Connection refused",
        )

        # Should match pattern
        query = LogQuery(pattern="refused")
        assert entry.matches_query(query)

        # Should match case-insensitive
        query = LogQuery(pattern="REFUSED", case_sensitive=False)
        assert entry.matches_query(query)

        # Should not match case-sensitive
        query = LogQuery(pattern="REFUSED", case_sensitive=True)
        assert not entry.matches_query(query)

    def test_log_entry_matches_query_time(self) -> None:
        """Test time range matching."""
        now = datetime.now()
        entry = LogEntry(
            container_id="abc",
            container_name="test",
            runtime="docker",
            message="Test",
            timestamp=now - timedelta(hours=1),
        )

        # Should match time range
        query = LogQuery(since=now - timedelta(hours=2))
        assert entry.matches_query(query)

        # Should not match future time
        query = LogQuery(since=now)
        assert not entry.matches_query(query)

    def test_retention_policy(self) -> None:
        """Test retention policy defaults."""
        policy = RetentionPolicy()
        assert policy.max_age_days == 7
        assert policy.max_size_mb == 100

    def test_log_config(self) -> None:
        """Test log config defaults."""
        config = LogConfig()
        assert config.enabled is True
        assert config.collect_interval == 5.0

    def test_log_stats(self) -> None:
        """Test log stats model."""
        stats = LogStats(
            container_name="test",
            total_entries=100,
            total_size_bytes=1024,
            entries_by_level={"error": 5, "info": 95},
        )
        assert stats.total_entries == 100
        assert stats.entries_by_level["error"] == 5


class TestParseLogLevel:
    """Tests for log level parsing."""

    def test_parse_error(self) -> None:
        """Test parsing error level."""
        assert parse_log_level("ERROR: Something failed") == LogLevel.ERROR
        assert parse_log_level("[ERROR] Connection lost") == LogLevel.ERROR

    def test_parse_warning(self) -> None:
        """Test parsing warning level."""
        assert parse_log_level("WARNING: Disk space low") == LogLevel.WARNING
        assert parse_log_level("[WARN] Memory usage high") == LogLevel.WARNING

    def test_parse_info(self) -> None:
        """Test parsing info level."""
        assert parse_log_level("INFO: Started service") == LogLevel.INFO
        assert parse_log_level("[INFO] Listening on port 8080") == LogLevel.INFO

    def test_parse_debug(self) -> None:
        """Test parsing debug level."""
        assert parse_log_level("DEBUG: Variable value=42") == LogLevel.DEBUG

    def test_parse_critical(self) -> None:
        """Test parsing critical level."""
        assert parse_log_level("CRITICAL: System failure") == LogLevel.CRITICAL
        assert parse_log_level("FATAL: Cannot continue") == LogLevel.CRITICAL

    def test_parse_unknown(self) -> None:
        """Test parsing unknown level."""
        assert parse_log_level("Just a regular message") == LogLevel.UNKNOWN


class TestLogStore:
    """Tests for LogStore."""

    def test_store_creation(self) -> None:
        """Test creating a log store."""
        with TemporaryDirectory() as tmpdir:
            store = LogStore(store_dir=Path(tmpdir))
            assert store.store_dir == Path(tmpdir)

    def test_append_and_flush(self) -> None:
        """Test appending and flushing logs."""
        with TemporaryDirectory() as tmpdir:
            store = LogStore(store_dir=Path(tmpdir))

            entry = LogEntry(
                container_id="abc",
                container_name="test",
                runtime="docker",
                message="Test message",
            )
            store.append("test", entry)
            store.flush("test")

            # Verify file exists
            log_file = Path(tmpdir) / "test.jsonl"
            assert log_file.exists()

    def test_query_logs(self) -> None:
        """Test querying stored logs."""
        with TemporaryDirectory() as tmpdir:
            store = LogStore(store_dir=Path(tmpdir))

            # Add entries
            for i in range(5):
                entry = LogEntry(
                    container_id="abc",
                    container_name="test",
                    runtime="docker",
                    message=f"Message {i}",
                    level=LogLevel.ERROR if i == 2 else LogLevel.INFO,
                )
                store.append("test", entry)
            store.flush("test")

            # Query all
            entries = store.query("test")
            assert len(entries) == 5

            # Query errors only
            query = LogQuery(levels=[LogLevel.ERROR])
            entries = store.query("test", query)
            assert len(entries) == 1
            assert "Message 2" in entries[0].message

    def test_list_containers(self) -> None:
        """Test listing containers with logs."""
        with TemporaryDirectory() as tmpdir:
            store = LogStore(store_dir=Path(tmpdir))

            # Add entries for multiple containers
            for name in ["container1", "container2"]:
                entry = LogEntry(
                    container_id="abc",
                    container_name=name,
                    runtime="docker",
                    message="Test",
                )
                store.append(name, entry)
                store.flush(name)

            containers = store.list_containers()
            assert "container1" in containers
            assert "container2" in containers

    def test_get_stats(self) -> None:
        """Test getting log statistics."""
        with TemporaryDirectory() as tmpdir:
            store = LogStore(store_dir=Path(tmpdir))

            # Add entries
            for i in range(10):
                entry = LogEntry(
                    container_id="abc",
                    container_name="test",
                    runtime="docker",
                    message=f"Message {i}",
                    level=LogLevel.ERROR if i < 3 else LogLevel.INFO,
                )
                store.append("test", entry)
            store.flush("test")

            stats = store.get_stats("test")
            assert stats.total_entries == 10
            assert stats.entries_by_level.get("error", 0) == 3

    def test_delete_logs(self) -> None:
        """Test deleting logs."""
        with TemporaryDirectory() as tmpdir:
            store = LogStore(store_dir=Path(tmpdir))

            entry = LogEntry(
                container_id="abc",
                container_name="test",
                runtime="docker",
                message="Test",
            )
            store.append("test", entry)
            store.flush("test")

            log_file = Path(tmpdir) / "test.jsonl"
            assert log_file.exists()

            store.delete("test")
            assert not log_file.exists()

    def test_export_logs(self) -> None:
        """Test exporting logs."""
        with TemporaryDirectory() as tmpdir:
            store = LogStore(store_dir=Path(tmpdir))

            for i in range(3):
                entry = LogEntry(
                    container_id="abc",
                    container_name="test",
                    runtime="docker",
                    message=f"Message {i}",
                )
                store.append("test", entry)
            store.flush("test")

            export_path = Path(tmpdir) / "export.txt"
            store.export("test", export_path, format="text")

            assert export_path.exists()
            content = export_path.read_text()
            assert "Message 0" in content
            assert "Message 2" in content


class TestLogSearcher:
    """Tests for LogSearcher."""

    @pytest.fixture
    def sample_entries(self) -> list[LogEntry]:
        """Create sample log entries."""
        now = datetime.now()
        return [
            LogEntry(
                container_id="abc",
                container_name="app1",
                runtime="docker",
                message="INFO: Application started",
                level=LogLevel.INFO,
                timestamp=now - timedelta(hours=2),
            ),
            LogEntry(
                container_id="abc",
                container_name="app1",
                runtime="docker",
                message="ERROR: Connection refused",
                level=LogLevel.ERROR,
                timestamp=now - timedelta(hours=1),
            ),
            LogEntry(
                container_id="def",
                container_name="app2",
                runtime="docker",
                message="WARNING: High memory usage",
                level=LogLevel.WARNING,
                timestamp=now - timedelta(minutes=30),
            ),
            LogEntry(
                container_id="abc",
                container_name="app1",
                runtime="docker",
                message="ERROR: Timeout waiting for response",
                level=LogLevel.ERROR,
                timestamp=now - timedelta(minutes=15),
            ),
        ]

    def test_filter_by_level(self, sample_entries) -> None:
        """Test filtering by log level."""
        searcher = LogSearcher(sample_entries)
        errors = searcher.filter_by_level(LogLevel.ERROR)
        assert len(errors) == 2

    def test_filter_by_container(self, sample_entries) -> None:
        """Test filtering by container."""
        searcher = LogSearcher(sample_entries)
        app1 = searcher.filter_by_container("app1")
        assert len(app1) == 3

    def test_search_text(self, sample_entries) -> None:
        """Test text search."""
        searcher = LogSearcher(sample_entries)
        matches = searcher.search("refused")
        assert len(matches) == 1
        assert "refused" in matches[0].message.lower()

    def test_search_regex(self, sample_entries) -> None:
        """Test regex search."""
        searcher = LogSearcher(sample_entries)
        matches = searcher.search(r"(ERROR|WARNING)", regex=True)
        assert len(matches) == 3

    def test_count_by_level(self, sample_entries) -> None:
        """Test counting by level."""
        searcher = LogSearcher(sample_entries)
        counts = searcher.count_by_level()
        assert counts[LogLevel.ERROR] == 2
        assert counts[LogLevel.INFO] == 1
        assert counts[LogLevel.WARNING] == 1

    def test_count_by_container(self, sample_entries) -> None:
        """Test counting by container."""
        searcher = LogSearcher(sample_entries)
        counts = searcher.count_by_container()
        assert counts["app1"] == 3
        assert counts["app2"] == 1

    def test_get_error_summary(self, sample_entries) -> None:
        """Test error summary."""
        searcher = LogSearcher(sample_entries)
        summary = searcher.get_error_summary()
        assert summary["total_errors"] == 2
        assert summary["total_warnings"] == 1

    def test_find_top_patterns(self, sample_entries) -> None:
        """Test finding top patterns."""
        # Add more entries with similar patterns
        entries = sample_entries + [
            LogEntry(
                container_id="abc",
                container_name="app1",
                runtime="docker",
                message="ERROR: Connection refused to host",
                level=LogLevel.ERROR,
            ),
        ]
        searcher = LogSearcher(entries)
        patterns = searcher.find_top_patterns(n=5, min_count=1)
        assert len(patterns) > 0


class TestCreateQuery:
    """Tests for query creation helper."""

    def test_create_simple_query(self) -> None:
        """Test creating a simple query."""
        query = create_query(pattern="error")
        assert query.pattern == "error"

    def test_create_query_with_time(self) -> None:
        """Test creating query with relative time."""
        query = create_query(since="1h")
        assert query.since is not None
        assert query.since > datetime.now() - timedelta(hours=2)

    def test_create_query_with_levels(self) -> None:
        """Test creating query with levels."""
        query = create_query(levels=["error", "warning"])
        assert len(query.levels) == 2
        assert LogLevel.ERROR in query.levels

    def test_create_query_with_iso_time(self) -> None:
        """Test creating query with ISO timestamp."""
        now = datetime.now()
        query = create_query(since=now.isoformat())
        assert query.since is not None


class TestLogCollector:
    """Tests for LogCollector."""

    def test_collector_creation(self) -> None:
        """Test creating a log collector."""
        mock_runtime = MagicMock()
        mock_runtime.name = "docker"
        collector = LogCollector(mock_runtime)
        assert collector.runtime == mock_runtime

    def test_collect_logs(self) -> None:
        """Test collecting logs from runtime."""
        mock_runtime = MagicMock()
        mock_runtime.name = "docker"
        mock_runtime.logs.return_value = "2025-01-12T10:00:00Z INFO: Started\n2025-01-12T10:01:00Z ERROR: Failed\n"

        collector = LogCollector(mock_runtime)
        entries = collector.collect("test-container")

        assert len(entries) == 2
        mock_runtime.logs.assert_called_once()

    def test_collect_with_callback(self) -> None:
        """Test collecting with callback."""
        mock_runtime = MagicMock()
        mock_runtime.name = "docker"
        mock_runtime.logs.return_value = "INFO: Test\n"

        collected = []

        def callback(entry: LogEntry) -> None:
            collected.append(entry)

        collector = LogCollector(mock_runtime)
        collector.add_callback(callback)
        collector.collect("test")

        assert len(collected) == 1

    def test_detect_source(self) -> None:
        """Test source detection by runtime."""
        for runtime_name, expected_source in [
            ("docker", LogSource.STDOUT),
            ("nspawn", LogSource.JOURNAL),
            ("libvirt", LogSource.FILE),
        ]:
            mock_runtime = MagicMock()
            mock_runtime.name = runtime_name
            mock_runtime.logs.return_value = "Test\n"

            collector = LogCollector(mock_runtime)
            entries = collector.collect("test")

            assert entries[0].source == expected_source


class TestCLILogs:
    """Tests for CLI log commands."""

    def test_logs_help(self) -> None:
        """Test logs subcommand help."""
        from typer.testing import CliRunner

        from gaol.cli import app

        runner = CliRunner()
        result = runner.invoke(app, ["log", "--help"])

        assert result.exit_code == 0
        assert "search" in result.output
        assert "stats" in result.output
        assert "collect" in result.output

    def test_logs_search_help(self) -> None:
        """Test logs search command help."""
        from typer.testing import CliRunner

        from gaol.cli import app

        runner = CliRunner()
        result = runner.invoke(app, ["log", "search", "--help"])

        assert result.exit_code == 0
        assert "--pattern" in result.output or "PATTERN" in result.output
        assert "--since" in result.output

    def test_logs_stats_empty(self) -> None:
        """Test logs stats with no logs."""
        from typer.testing import CliRunner

        from gaol.cli import app

        with TemporaryDirectory() as tmpdir:
            with patch("gaol.logs.store.get_log_store_dir") as mock_dir:
                mock_dir.return_value = Path(tmpdir)

                runner = CliRunner()
                result = runner.invoke(app, ["log", "stats"])

                assert result.exit_code == 0
                assert "No stored logs" in result.output
