"""Tests for CLI log handlers - JSON message parsing and output formatting."""

from __future__ import annotations

import json
from io import StringIO
from unittest.mock import patch

from terminaluse.lib.cli.handlers.log_handlers import (
    _enrich_log_entry,
    _filter_by_min_level,
    _format_timestamp,
    _get_effective_level,
    _get_effective_timestamp,
    _parse_json_message,
    _parse_log_message,
)

# =============================================================================
# JSON Message Parsing Tests
# =============================================================================


class TestParseJsonMessage:
    """Test extraction of inner fields from JSON log messages."""

    def test_extracts_standard_fields(self):
        """Standard structured log with ts, level, msg, logger."""
        message = json.dumps(
            {
                "ts": "2026-01-31T04:50:01.123Z",
                "level": "INFO",
                "msg": "HTTP Request OK",
                "logger": "httpx",
            }
        )
        result = _parse_json_message(message)

        assert result is not None
        assert result["ts"] == "2026-01-31T04:50:01.123Z"
        assert result["level"] == "INFO"
        assert result["msg"] == "HTTP Request OK"
        assert result["logger"] == "httpx"

    def test_extracts_alternative_field_names(self):
        """Support for timestamp, message, lvl, name variations."""
        message = json.dumps(
            {
                "timestamp": "2026-01-31T04:50:01Z",
                "lvl": "error",
                "message": "Connection failed",
                "name": "database",
            }
        )
        result = _parse_json_message(message)

        assert result is not None
        assert result["ts"] == "2026-01-31T04:50:01Z"
        assert result["level"] == "ERROR"  # Normalized to uppercase
        assert result["msg"] == "Connection failed"
        assert result["logger"] == "database"

    def test_extracts_elasticsearch_style_fields(self):
        """Support for @timestamp and severity."""
        message = json.dumps(
            {
                "@timestamp": "2026-01-31T04:50:01Z",
                "severity": "warning",
                "text": "Slow query detected",
            }
        )
        result = _parse_json_message(message)

        assert result is not None
        assert result["ts"] == "2026-01-31T04:50:01Z"
        assert result["level"] == "WARNING"
        assert result["msg"] == "Slow query detected"

    def test_returns_none_for_plain_text(self):
        """Plain text messages return None."""
        result = _parse_json_message("Just a plain log message")
        assert result is None

    def test_returns_none_for_invalid_json(self):
        """Invalid JSON returns None."""
        result = _parse_json_message("{not valid json")
        assert result is None

    def test_returns_none_for_json_array(self):
        """JSON arrays are not log objects."""
        result = _parse_json_message("[1, 2, 3]")
        assert result is None

    def test_returns_none_for_json_without_msg(self):
        """JSON without a message field is not useful."""
        message = json.dumps(
            {
                "ts": "2026-01-31T04:50:01Z",
                "level": "INFO",
                # No msg/message/text field
            }
        )
        result = _parse_json_message(message)
        assert result is None

    def test_partial_fields_extracted(self):
        """Only msg is required; other fields are optional."""
        message = json.dumps({"msg": "Something happened"})
        result = _parse_json_message(message)

        assert result is not None
        assert result["msg"] == "Something happened"
        assert "ts" not in result
        assert "level" not in result
        assert "logger" not in result

    def test_level_normalization(self):
        """Levels are normalized to uppercase."""
        for input_level, expected in [
            ("info", "INFO"),
            ("INFO", "INFO"),
            ("Info", "INFO"),
            ("warning", "WARNING"),
            ("error", "ERROR"),
            ("debug", "DEBUG"),
        ]:
            message = json.dumps({"level": input_level, "msg": "test"})
            result = _parse_json_message(message)
            assert result["level"] == expected

    def test_handles_empty_string(self):
        """Empty string returns None."""
        result = _parse_json_message("")
        assert result is None

    def test_handles_json_primitives(self):
        """JSON primitives (string, number, null) return None."""
        assert _parse_json_message('"just a string"') is None
        assert _parse_json_message("123") is None
        assert _parse_json_message("null") is None
        assert _parse_json_message("true") is None


# =============================================================================
# Text Format Log Parsing Tests
# =============================================================================


class TestParseLogMessage:
    """Test _parse_log_message which tries both JSON and text formats."""

    def test_parses_json_format(self):
        """JSON format is parsed correctly."""
        message = json.dumps(
            {
                "ts": "2026-01-31T04:50:01.123Z",
                "level": "INFO",
                "msg": "HTTP Request OK",
            }
        )
        result = _parse_log_message(message)

        assert result is not None
        assert result["ts"] == "2026-01-31T04:50:01.123Z"
        assert result["level"] == "INFO"
        assert result["msg"] == "HTTP Request OK"

    def test_parses_text_format_with_z_suffix(self):
        """Text format: ISO_TIMESTAMP [LEVEL] message"""
        message = "2026-01-31T04:50:06.724Z [DEBUG] Getting matching hook commands for Stop..."
        result = _parse_log_message(message)

        assert result is not None
        assert result["ts"] == "2026-01-31T04:50:06.724Z"
        assert result["level"] == "DEBUG"
        assert result["msg"] == "Getting matching hook commands for Stop..."

    def test_parses_text_format_without_z_suffix(self):
        """Text format without Z suffix."""
        message = "2026-01-31T04:50:06.724 [ERROR] Connection failed"
        result = _parse_log_message(message)

        assert result is not None
        assert result["ts"] == "2026-01-31T04:50:06.724"
        assert result["level"] == "ERROR"
        assert result["msg"] == "Connection failed"

    def test_text_format_normalizes_level(self):
        """Text format levels are normalized to uppercase."""
        message = "2026-01-31T04:50:06.724Z [warning] Something happened"
        result = _parse_log_message(message)

        assert result is not None
        assert result["level"] == "WARNING"

    def test_returns_none_for_plain_text(self):
        """Plain text without structure returns None."""
        result = _parse_log_message("Just a plain message")
        assert result is None

    def test_returns_none_for_partial_match(self):
        """Partial matches don't extract."""
        # Missing level brackets
        result = _parse_log_message("2026-01-31T04:50:06.724Z DEBUG message")
        assert result is None

        # Missing timestamp
        result = _parse_log_message("[DEBUG] message without timestamp")
        assert result is None

    def test_json_takes_priority_over_text(self):
        """If both could match, JSON is tried first."""
        # This is valid JSON with a msg field
        message = json.dumps({"msg": "test", "level": "INFO"})
        result = _parse_log_message(message)

        assert result is not None
        assert result["msg"] == "test"
        assert result["level"] == "INFO"


# =============================================================================
# Effective Timestamp Tests
# =============================================================================


class TestGetEffectiveTimestamp:
    """Test extraction of effective timestamp for sorting."""

    def test_returns_parsed_ts_for_json(self):
        """JSON messages return parsed inner timestamp."""
        log = {
            "timestamp": "2026-01-31T04:50:08.034Z",
            "message": json.dumps(
                {
                    "ts": "2026-01-31T04:50:01.123Z",
                    "msg": "test",
                }
            ),
        }
        result = _get_effective_timestamp(log)
        assert result == "2026-01-31T04:50:01.123Z"

    def test_returns_parsed_ts_for_text_format(self):
        """Text format messages return parsed timestamp."""
        log = {
            "timestamp": "2026-01-31T04:50:08.034Z",
            "message": "2026-01-31T04:50:06.724Z [DEBUG] Something happened",
        }
        result = _get_effective_timestamp(log)
        assert result == "2026-01-31T04:50:06.724Z"

    def test_returns_envelope_for_plain_text(self):
        """Plain text returns envelope timestamp."""
        log = {
            "timestamp": "2026-01-31T04:50:08.034Z",
            "message": "Plain text message",
        }
        result = _get_effective_timestamp(log)
        assert result == "2026-01-31T04:50:08.034Z"

    def test_returns_empty_string_if_no_timestamp(self):
        """Missing timestamp returns empty string."""
        log = {"message": "No timestamp anywhere"}
        result = _get_effective_timestamp(log)
        assert result == ""


# =============================================================================
# Log Entry Enrichment Tests
# =============================================================================


class TestEnrichLogEntry:
    """Test enrichment of log entries with parsed fields."""

    def test_enriches_json_message(self):
        """JSON messages get parsed_* fields added."""
        log = {
            "timestamp": "2026-01-31T04:50:08.034Z",
            "level": "WARNING",
            "source": "stderr",
            "message": json.dumps(
                {
                    "ts": "2026-01-31T04:50:01.123Z",
                    "level": "INFO",
                    "msg": "HTTP Request OK",
                    "logger": "httpx",
                }
            ),
        }

        enriched = _enrich_log_entry(log)

        # Original fields preserved
        assert enriched["timestamp"] == "2026-01-31T04:50:08.034Z"
        assert enriched["level"] == "WARNING"
        assert enriched["source"] == "stderr"
        assert enriched["message"] == log["message"]

        # Parsed fields added
        assert enriched["parsed_ts"] == "2026-01-31T04:50:01.123Z"
        assert enriched["parsed_level"] == "INFO"
        assert enriched["parsed_msg"] == "HTTP Request OK"
        assert enriched["parsed_logger"] == "httpx"

    def test_plain_text_not_enriched(self):
        """Plain text messages are returned unchanged."""
        log = {
            "timestamp": "2026-01-31T04:50:08.034Z",
            "level": "INFO",
            "source": "stdout",
            "message": "Plain text log message",
        }

        enriched = _enrich_log_entry(log)

        assert enriched == log
        assert "parsed_ts" not in enriched
        assert "parsed_level" not in enriched
        assert "parsed_msg" not in enriched

    def test_does_not_mutate_original(self):
        """Enrichment creates a copy, doesn't mutate original."""
        log = {
            "message": json.dumps({"msg": "test"}),
        }
        original_message = log["message"]

        enriched = _enrich_log_entry(log)

        assert "parsed_msg" in enriched
        assert "parsed_msg" not in log
        assert log["message"] == original_message

    def test_enriches_text_format_message(self):
        """Text format messages get parsed_* fields added."""
        log = {
            "timestamp": "2026-01-31T04:50:08.034Z",
            "level": "WARNING",
            "source": "stderr",
            "message": "2026-01-31T04:50:06.724Z [DEBUG] Getting matching hook commands...",
        }

        enriched = _enrich_log_entry(log)

        # Original fields preserved
        assert enriched["timestamp"] == "2026-01-31T04:50:08.034Z"
        assert enriched["level"] == "WARNING"

        # Parsed fields added
        assert enriched["parsed_ts"] == "2026-01-31T04:50:06.724Z"
        assert enriched["parsed_level"] == "DEBUG"
        assert enriched["parsed_msg"] == "Getting matching hook commands..."


# =============================================================================
# Timestamp Formatting Tests
# =============================================================================


class TestFormatTimestamp:
    """Test timestamp formatting for display."""

    def test_formats_iso_with_z_suffix(self):
        """ISO timestamp with Z suffix."""
        result = _format_timestamp("2026-01-31T04:50:01.123000Z")
        assert result == "04:50:01.123"

    def test_formats_iso_with_offset(self):
        """ISO timestamp with timezone offset."""
        result = _format_timestamp("2026-01-31T04:50:01.456000+00:00")
        assert result == "04:50:01.456"

    def test_formats_iso_without_microseconds(self):
        """ISO timestamp without microseconds shows .000."""
        result = _format_timestamp("2026-01-31T04:50:01Z")
        assert result == "04:50:01.000"

    def test_handles_invalid_timestamp(self):
        """Invalid timestamps are truncated gracefully."""
        result = _format_timestamp("not-a-timestamp")
        assert result == "not-a-timest"  # Truncated to 12 chars

    def test_handles_short_invalid_timestamp(self):
        """Short invalid timestamps returned as-is."""
        result = _format_timestamp("short")
        assert result == "short"

    def test_handles_non_string(self):
        """Non-string timestamps converted to string."""
        result = _format_timestamp(12345)
        assert result == "12345"

    def test_formats_with_date_when_requested(self):
        """Include date when include_date=True."""
        result = _format_timestamp("2026-01-31T04:50:01.123000Z", include_date=True)
        assert result == "Jan 31 04:50:01.123"

    def test_formats_without_date_by_default(self):
        """Date is omitted by default."""
        result = _format_timestamp("2026-01-31T04:50:01.123000Z", include_date=False)
        assert result == "04:50:01.123"


# =============================================================================
# Integration: TTY Output Tests
# =============================================================================


class TestTtyOutputIntegration:
    """Integration tests for TTY output with JSON parsing."""

    def test_json_message_displayed_as_inner_content(self):
        """JSON messages show inner msg, level, timestamp in TTY output."""
        from rich.console import Console

        from terminaluse.lib.cli.handlers.log_handlers import _print_log_line

        log = {
            "timestamp": "2026-01-31T04:50:08.034Z",
            "level": "WARNING",
            "source": "stderr",
            "message": json.dumps(
                {
                    "ts": "2026-01-31T04:50:01.123Z",
                    "level": "INFO",
                    "msg": "HTTP Request OK",
                    "logger": "httpx",
                }
            ),
            "logger": "raw",
        }

        # Capture output (no_color=True to avoid ANSI escape codes)
        output = StringIO()
        with patch("terminaluse.lib.cli.handlers.log_handlers.console", Console(file=output, no_color=True)):
            _print_log_line(log)

        result = output.getvalue()

        # Should show inner timestamp (04:50:01.123), not envelope (04:50:08.034)
        assert "04:50:01.123" in result
        assert "04:50:08.034" not in result

        # Should show inner level (INFO), not envelope (WARNING)
        assert "INFO" in result

        # Should show inner message, not raw JSON
        assert "HTTP Request OK" in result
        assert '{"ts":' not in result

        # Should show inner logger
        assert "httpx" in result

    def test_plain_text_uses_envelope_values(self):
        """Plain text messages use envelope timestamp/level."""
        from rich.console import Console

        from terminaluse.lib.cli.handlers.log_handlers import _print_log_line

        log = {
            "timestamp": "2026-01-31T04:50:08.034Z",
            "level": "WARNING",
            "source": "stderr",
            "message": "Plain error message",
            "logger": "",
        }

        # Capture output (no_color=True to avoid ANSI escape codes)
        output = StringIO()
        with patch("terminaluse.lib.cli.handlers.log_handlers.console", Console(file=output, no_color=True)):
            _print_log_line(log)

        result = output.getvalue()

        # Should show envelope values
        assert "04:50:08.034" in result
        assert "WARNING" in result
        assert "Plain error message" in result

    def test_text_format_displayed_as_inner_content(self):
        """Text format logs show inner level, timestamp, message."""
        from rich.console import Console

        from terminaluse.lib.cli.handlers.log_handlers import _print_log_line

        log = {
            "timestamp": "2026-01-31T04:50:08.034Z",
            "level": "WARNING",
            "source": "stderr",
            "message": "2026-01-31T04:50:06.724Z [DEBUG] Getting matching hook commands...",
            "logger": "raw",
        }

        # Capture output (no_color=True to avoid ANSI escape codes)
        output = StringIO()
        with patch("terminaluse.lib.cli.handlers.log_handlers.console", Console(file=output, no_color=True)):
            _print_log_line(log)

        result = output.getvalue()

        # Should show inner timestamp (04:50:06.724), not envelope (04:50:08.034)
        assert "04:50:06.724" in result
        assert "04:50:08.034" not in result

        # Should show inner level (DEBUG), not envelope (WARNING)
        assert "DEBUG" in result
        assert "WARNING" not in result

        # Should show inner message
        assert "Getting matching hook commands..." in result


# =============================================================================
# Level Filtering Tests
# =============================================================================


class TestGetEffectiveLevel:
    """Test extraction of effective log level."""

    def test_returns_parsed_level_for_json(self):
        """JSON messages return parsed inner level."""
        log = {
            "level": "WARNING",
            "message": json.dumps({"level": "INFO", "msg": "test"}),
        }
        result = _get_effective_level(log)
        assert result == "INFO"

    def test_returns_parsed_level_for_text_format(self):
        """Text format messages return parsed level."""
        log = {
            "level": "WARNING",
            "message": "2026-01-31T04:50:06.724Z [DEBUG] Something happened",
        }
        result = _get_effective_level(log)
        assert result == "DEBUG"

    def test_returns_envelope_level_for_plain_text(self):
        """Plain text returns envelope level."""
        log = {
            "level": "ERROR",
            "message": "Plain text message",
        }
        result = _get_effective_level(log)
        assert result == "ERROR"

    def test_returns_info_if_no_level(self):
        """Missing level defaults to INFO."""
        log = {"message": "No level anywhere"}
        result = _get_effective_level(log)
        assert result == "INFO"


class TestFilterByMinLevel:
    """Test minimum-severity level filtering."""

    def _make_log(self, level: str, message: str | None = None) -> dict:
        """Helper to create a log entry with a given level."""
        if message is None:
            message = f"Log at {level}"
        return {"level": level, "message": message}

    def test_debug_includes_all_levels(self):
        """DEBUG minimum includes all severity levels."""
        logs = [
            self._make_log("DEBUG"),
            self._make_log("INFO"),
            self._make_log("WARNING"),
            self._make_log("ERROR"),
            self._make_log("CRITICAL"),
        ]
        result = _filter_by_min_level(logs, "DEBUG")
        assert len(result) == 5

    def test_warning_excludes_debug_and_info(self):
        """WARNING minimum excludes DEBUG and INFO."""
        logs = [
            self._make_log("DEBUG"),
            self._make_log("INFO"),
            self._make_log("WARNING"),
            self._make_log("ERROR"),
            self._make_log("CRITICAL"),
        ]
        result = _filter_by_min_level(logs, "WARNING")
        assert len(result) == 3
        levels = [log["level"] for log in result]
        assert "WARNING" in levels
        assert "ERROR" in levels
        assert "CRITICAL" in levels
        assert "DEBUG" not in levels
        assert "INFO" not in levels

    def test_error_only_includes_error_and_critical(self):
        """ERROR minimum only includes ERROR and CRITICAL."""
        logs = [
            self._make_log("DEBUG"),
            self._make_log("INFO"),
            self._make_log("WARNING"),
            self._make_log("ERROR"),
            self._make_log("CRITICAL"),
        ]
        result = _filter_by_min_level(logs, "ERROR")
        assert len(result) == 2
        levels = [log["level"] for log in result]
        assert levels == ["ERROR", "CRITICAL"]

    def test_critical_only_includes_critical(self):
        """CRITICAL minimum only includes CRITICAL."""
        logs = [
            self._make_log("DEBUG"),
            self._make_log("INFO"),
            self._make_log("WARNING"),
            self._make_log("ERROR"),
            self._make_log("CRITICAL"),
        ]
        result = _filter_by_min_level(logs, "CRITICAL")
        assert len(result) == 1
        assert result[0]["level"] == "CRITICAL"

    def test_uses_effective_level_from_json_message(self):
        """Filtering uses parsed level from JSON messages."""
        logs = [
            # Envelope says WARNING, but inner JSON says DEBUG
            {
                "level": "WARNING",
                "message": json.dumps({"level": "DEBUG", "msg": "debug log"}),
            },
            # Envelope says WARNING, but inner JSON says ERROR
            {
                "level": "WARNING",
                "message": json.dumps({"level": "ERROR", "msg": "error log"}),
            },
        ]
        result = _filter_by_min_level(logs, "WARNING")
        # Only the ERROR one should pass (DEBUG is below WARNING)
        assert len(result) == 1
        assert "error log" in result[0]["message"]

    def test_uses_effective_level_from_text_format(self):
        """Filtering uses parsed level from text format messages."""
        logs = [
            # Envelope says WARNING, but text format says INFO
            {
                "level": "WARNING",
                "message": "2026-01-31T04:50:06Z [INFO] info log",
            },
            # Envelope says WARNING, but text format says ERROR
            {
                "level": "WARNING",
                "message": "2026-01-31T04:50:06Z [ERROR] error log",
            },
        ]
        result = _filter_by_min_level(logs, "WARNING")
        # Only the ERROR one should pass
        assert len(result) == 1
        assert "error log" in result[0]["message"]

    def test_empty_logs_returns_empty(self):
        """Empty input returns empty output."""
        result = _filter_by_min_level([], "WARNING")
        assert result == []
