"""Tests for SDK log sender: log entry building and OTLP formatting.

The SDK parses JSON logs from the sandbox, extracts metadata (level, logger, timestamp),
and sends logs to Nucleus in OTLP format.
"""

from __future__ import annotations

import json
import logging
from unittest.mock import patch

import pytest

from terminaluse.lib.logging.log_sender import (
    LogSender,
    _logs_to_otlp,
)
from terminaluse.lib.sandbox.entrypoint import _StructuredLogFormatter

# =============================================================================
# Log entry building tests
# =============================================================================


class TestLogEntryBuilding:
    """Test that log entries are built correctly from stdout/stderr."""

    def test_plain_text_creates_entry(self):
        sender = LogSender(nucleus_url="http://localhost:8000", agent_api_key="test")
        with patch.object(sender, "_get_otel_context", return_value=(None, None)):
            logs = sender._build_log_entries("event/send", "hello world", "", None)
        assert len(logs) == 1
        assert logs[0]["source"] == "stdout"
        assert logs[0]["level"] == "INFO"
        assert logs[0]["message"] == "hello world"

    def test_stderr_defaults_to_warning(self):
        sender = LogSender(nucleus_url="http://localhost:8000", agent_api_key="test")
        with patch.object(sender, "_get_otel_context", return_value=(None, None)):
            logs = sender._build_log_entries("event/send", "", "error output", None)
        assert logs[0]["level"] == "WARNING"
        assert logs[0]["source"] == "stderr"

    def test_multiple_lines_create_multiple_entries(self):
        sender = LogSender(nucleus_url="http://localhost:8000", agent_api_key="test")
        with patch.object(sender, "_get_otel_context", return_value=(None, None)):
            logs = sender._build_log_entries("event/send", "line1\nline2\nline3", "", None)
        assert len(logs) == 3

    def test_empty_lines_skipped(self):
        sender = LogSender(nucleus_url="http://localhost:8000", agent_api_key="test")
        with patch.object(sender, "_get_otel_context", return_value=(None, None)):
            logs = sender._build_log_entries("event/send", "line1\n\n\nline2", "", None)
        assert len(logs) == 2

    def test_json_line_parsed_for_metadata(self):
        """SDK parses JSON logs to extract level and logger."""
        sender = LogSender(nucleus_url="http://localhost:8000", agent_api_key="test")
        line = '{"ts":"2026-01-29T05:04:06Z","level":"INFO","logger":"httpx","msg":"ok"}'
        with patch.object(sender, "_get_otel_context", return_value=(None, None)):
            logs = sender._build_log_entries("event/send", line, "", None)
        assert len(logs) == 1
        assert logs[0]["level"] == "INFO"  # Extracted from JSON
        assert logs[0]["logger"] == "httpx"  # Extracted from JSON
        assert logs[0]["message"] == "ok"  # Extracted from JSON

    def test_stack_trace_lines_sent_individually(self):
        """Each line of a stack trace is a separate entry."""
        sender = LogSender(nucleus_url="http://localhost:8000", agent_api_key="test")
        stderr = "\n".join(
            [
                "2026-01-29T05:04:04.941Z [ERROR] Error: ENOENT: no such file",
                "    at ax1 (/$bunfs/root/claude:4791:4342)",
                "    at A (/$bunfs/root/claude:11:7259)",
            ]
        )
        with patch.object(sender, "_get_otel_context", return_value=(None, None)):
            logs = sender._build_log_entries("event/send", "", stderr, None)
        assert len(logs) == 3
        assert logs[0]["message"] == "2026-01-29T05:04:04.941Z [ERROR] Error: ENOENT: no such file"
        assert logs[1]["message"] == "    at ax1 (/$bunfs/root/claude:4791:4342)"
        assert logs[2]["message"] == "    at A (/$bunfs/root/claude:11:7259)"


# =============================================================================
# OTLP formatting tests
# =============================================================================


class TestOtlpFormatting:
    """Test the OTLP wire format."""

    def _make_log(self, message="hello", source="stdout", logger=None):
        return {
            "log_id": "test-1",
            "timestamp": "2026-01-29T05:04:06+00:00",
            "task_id": "task-1",
            "method": "event/send",
            "trace_id": None,
            "span_id": None,
            "source": source,
            "level": "INFO",
            "logger": logger,
            "message": message,
        }

    def test_logs_grouped_by_logger(self):
        logs = [self._make_log(logger="httpx"), self._make_log(logger="__main__")]
        otlp = _logs_to_otlp(logs, task_id="task-1", method="event/send")
        scope_logs = otlp["resourceLogs"][0]["scopeLogs"]
        # Each logger gets its own scope
        assert len(scope_logs) == 2
        scope_names = {s["scope"]["name"] for s in scope_logs}
        assert scope_names == {"httpx", "__main__"}

    def test_logs_without_logger_grouped_under_unknown(self):
        logs = [self._make_log(logger=None)]
        otlp = _logs_to_otlp(logs, task_id="task-1", method="event/send")
        scope_logs = otlp["resourceLogs"][0]["scopeLogs"]
        assert len(scope_logs) == 1
        assert scope_logs[0]["scope"]["name"] == "unknown"

    def test_source_attribute(self):
        logs = [self._make_log(source="stderr")]
        otlp = _logs_to_otlp(logs, task_id="task-1", method="event/send")
        records = otlp["resourceLogs"][0]["scopeLogs"][0]["logRecords"]
        attrs = {a["key"]: a["value"] for a in records[0]["attributes"]}
        assert attrs["log.source"]["stringValue"] == "stderr"

    def test_empty_logs_returns_empty_resource_logs(self):
        otlp = _logs_to_otlp([], task_id=None, method="")
        assert otlp == {"resourceLogs": []}

    def test_resource_attributes_include_task_and_method(self):
        logs = [self._make_log()]
        otlp = _logs_to_otlp(logs, task_id="task-42", method="event/send")
        resource_attrs = otlp["resourceLogs"][0]["resource"]["attributes"]
        attr_map = {a["key"]: a["value"]["stringValue"] for a in resource_attrs}
        assert attr_map["service.name"] == "agent-sdk"
        assert attr_map["agent.task_id"] == "task-42"
        assert attr_map["agent.method"] == "event/send"

    def test_trace_context_included(self):
        log = self._make_log()
        log["trace_id"] = "abc123"
        log["span_id"] = "def456"
        otlp = _logs_to_otlp([log], task_id="task-1", method="event/send")
        record = otlp["resourceLogs"][0]["scopeLogs"][0]["logRecords"]
        assert "traceId" in record[0]
        assert "spanId" in record[0]


# =============================================================================
# Integration: full pipeline test
# =============================================================================


class TestBuildLogEntriesIntegration:
    """Integration test for the full _build_log_entries pipeline."""

    def test_mixed_stderr_with_json_and_plaintext(self):
        """SDK parses JSON logs for metadata and extracts levels from plain text."""
        stderr = "\n".join(
            [
                '{"ts":"2026-01-29T05:04:03Z","level":"INFO","logger":"httpx","msg":"HTTP Request OK"}',
                "2026-01-29T05:04:04.941Z [ERROR] Error: ENOENT: no such file",
                "    at ax1 (/$bunfs/root/claude:4791:4342)",
                "    at A (/$bunfs/root/claude:11:7259)",
                "    at readdirSync (unknown)",
                '{"ts":"2026-01-29T05:04:05Z","level":"INFO","logger":"__main__","msg":"Handler completed"}',
            ]
        )

        sender = LogSender(nucleus_url="http://localhost:8000", agent_api_key="test")

        with patch.object(sender, "_get_otel_context", return_value=(None, None)):
            logs = sender._build_log_entries(
                method="event/send",
                stdout="",
                stderr=stderr,
                task_id="task-123",
            )

        # All 6 lines sent as individual entries
        assert len(logs) == 6
        assert all(entry["source"] == "stderr" for entry in logs)

        # JSON logs have their level extracted
        assert logs[0]["level"] == "INFO"  # From JSON
        assert logs[0]["logger"] == "httpx"  # From JSON

        # Plain text logs with level markers get level extracted
        assert logs[1]["level"] == "ERROR"  # Parsed from "[ERROR]" in message
        assert logs[2]["level"] == "WARNING"  # No marker, falls back to stderr default

    def test_stdout_plain_text_creates_entries(self):
        stdout = "some plain output\nmore output"

        sender = LogSender(nucleus_url="http://localhost:8000", agent_api_key="test")

        with patch.object(sender, "_get_otel_context", return_value=(None, None)):
            logs = sender._build_log_entries(
                method="event/send",
                stdout=stdout,
                stderr="",
                task_id=None,
            )

        assert len(logs) == 2
        assert logs[0]["source"] == "stdout"
        assert logs[0]["level"] == "INFO"
        assert logs[0]["message"] == "some plain output"
        assert logs[1]["message"] == "more output"


# =============================================================================
# StructuredLogFormatter exception tests
# =============================================================================


class TestStructuredLogFormatter:
    """Test that StructuredLogFormatter outputs valid JSON with expected fields."""

    def test_outputs_valid_json(self):
        formatter = _StructuredLogFormatter()
        logger_instance = logging.getLogger("test.formatter")

        record = logger_instance.makeRecord(
            name="test.formatter",
            level=logging.INFO,
            fn="test.py",
            lno=1,
            msg="All good",
            args=(),
            exc_info=None,
        )

        formatted = formatter.format(record)
        data = json.loads(formatted)

        assert data["level"] == "INFO"
        assert data["logger"] == "test.formatter"
        assert data["msg"] == "All good"
        assert "ts" in data  # Has timestamp

    def test_error_level_captured(self):
        formatter = _StructuredLogFormatter()
        logger_instance = logging.getLogger("test.formatter")

        record = logger_instance.makeRecord(
            name="test.formatter",
            level=logging.ERROR,
            fn="test.py",
            lno=1,
            msg="Something failed",
            args=(),
            exc_info=None,
        )

        formatted = formatter.format(record)
        data = json.loads(formatted)

        assert data["level"] == "ERROR"
        assert data["msg"] == "Something failed"


# =============================================================================
# HTTPS validation tests
# =============================================================================


class TestHttpsValidation:
    """Test HTTPS enforcement and development URL exceptions.

    The LogSender enforces HTTPS for API key protection, with exceptions
    for recognized development/internal endpoints. These tests verify both
    the allowed patterns and security against bypass attempts.
    """

    # -------------------------------------------------------------------------
    # Allowed URLs (should NOT raise ValueError)
    # -------------------------------------------------------------------------

    def test_https_url_allowed(self):
        """HTTPS URLs should always be allowed."""
        sender = LogSender(nucleus_url="https://api.terminaluse.com", agent_api_key="test")
        assert sender.nucleus_url == "https://api.terminaluse.com"

    def test_localhost_http_allowed(self):
        """HTTP localhost should be allowed for development."""
        sender = LogSender(nucleus_url="http://localhost:8000", agent_api_key="test")
        assert sender.nucleus_url == "http://localhost:8000"

    def test_127_0_0_1_http_allowed(self):
        """HTTP 127.0.0.1 should be allowed for development."""
        sender = LogSender(nucleus_url="http://127.0.0.1:5003", agent_api_key="test")
        assert sender.nucleus_url == "http://127.0.0.1:5003"

    def test_k8s_internal_http_allowed(self):
        """HTTP Kubernetes internal services should be allowed."""
        sender = LogSender(nucleus_url="http://nucleus.default.svc.cluster.local:8000", agent_api_key="test")
        assert sender.nucleus_url == "http://nucleus.default.svc.cluster.local:8000"

    def test_host_docker_internal_http_allowed(self):
        """HTTP host.docker.internal should be allowed for Docker Desktop development."""
        sender = LogSender(nucleus_url="http://host.docker.internal:5003", agent_api_key="test")
        assert sender.nucleus_url == "http://host.docker.internal:5003"

    def test_host_minikube_internal_http_allowed(self):
        """HTTP host.minikube.internal should be allowed for minikube development."""
        sender = LogSender(nucleus_url="http://host.minikube.internal:5003", agent_api_key="test")
        assert sender.nucleus_url == "http://host.minikube.internal:5003"

    # -------------------------------------------------------------------------
    # Rejected URLs (should raise ValueError)
    # -------------------------------------------------------------------------

    def test_http_production_url_rejected(self):
        """HTTP to production domains should be rejected."""
        with pytest.raises(ValueError, match="must use HTTPS"):
            LogSender(nucleus_url="http://api.terminaluse.com", agent_api_key="test")

    def test_http_random_domain_rejected(self):
        """HTTP to random domains should be rejected."""
        with pytest.raises(ValueError, match="must use HTTPS"):
            LogSender(nucleus_url="http://example.com:8000", agent_api_key="test")

    def test_http_ip_address_rejected(self):
        """HTTP to non-localhost IP addresses should be rejected."""
        with pytest.raises(ValueError, match="must use HTTPS"):
            LogSender(nucleus_url="http://192.168.1.100:8000", agent_api_key="test")
