"""Tests for SDK log sender: raw line forwarding and OTLP formatting.

The SDK is a dumb pipe — all lines (stdout/stderr) are forwarded raw with
sequence numbers. JSON detection, multiline coalescing, and timestamp/level
parsing are handled by the backend.
"""

from __future__ import annotations

import json
import logging
from unittest.mock import patch

import pytest

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

# =============================================================================
# Raw line sending tests
# =============================================================================


class TestRawLineSending:
    """Test that all lines are sent as raw entries with sequence numbers."""

    def test_plain_text_creates_raw_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]["seq"] == 0
        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_sequence_numbers_increment(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 [log["seq"] for log in logs] == [0, 1, 2]

    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_seq_spans_stdout_and_stderr(self):
        """Sequence numbers are global across stdout and stderr."""
        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", "out1\nout2", "err1", None)
        seqs = [entry["seq"] for entry in logs]
        assert seqs == [0, 1, 2]

    def test_json_line_sent_as_raw(self):
        """SDK does NOT parse JSON — it's sent as a raw line for backend processing."""
        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]["message"] == line  # raw, not parsed
        assert logs[0]["level"] == "WARNING"  # stderr default, not parsed from JSON
        assert logs[0]["seq"] == 0

    def test_stack_trace_sent_as_individual_lines(self):
        """SDK does NOT coalesce — each line 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)"
        assert [entry["seq"] for entry in logs] == [0, 1, 2]


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


class TestOtlpFormatting:
    """Test the OTLP wire format — all logs under single 'raw' scope."""

    def _make_log(self, seq=0, message="hello", source="stdout"):
        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",
            "message": message,
            "seq": seq,
        }

    def test_all_logs_under_raw_scope(self):
        logs = [self._make_log(0), self._make_log(1)]
        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"] == "raw"
        assert len(scope_logs[0]["logRecords"]) == 2

    def test_seq_attribute_on_every_record(self):
        logs = [self._make_log(seq=5)]
        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 "log.seq" in attrs
        assert attrs["log.seq"]["intValue"] == "5"

    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_sdk_version_resource_attribute(self):
        logs = [self._make_log()]
        otlp = _logs_to_otlp(logs, task_id="task-1", method="event/send")
        resource_attrs = otlp["resourceLogs"][0]["resource"]["attributes"]
        attr_map = {a["key"]: a["value"]["stringValue"] for a in resource_attrs}
        assert "terminaluse.sdk.version" in attr_map

    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"][0]
        assert "traceId" in record
        assert "spanId" in record


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


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

    def test_mixed_stderr_with_json_plaintext_and_stack_trace(self):
        """SDK sends all lines as raw entries — no coalescing, no JSON parsing."""
        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 raw entries
        assert len(logs) == 6
        assert all(entry["source"] == "stderr" for entry in logs)
        assert all(entry["level"] == "WARNING" for entry in logs)  # stderr default for ALL
        assert [entry["seq"] for entry in logs] == [0, 1, 2, 3, 4, 5]

        # Messages preserved exactly as-is
        assert logs[0]["message"].startswith("{")  # JSON, but not parsed
        assert logs[1]["message"] == "2026-01-29T05:04:04.941Z [ERROR] Error: ENOENT: no such file"
        assert logs[2]["message"] == "    at ax1 (/$bunfs/root/claude:4791:4342)"

    def test_stdout_plain_text_creates_raw_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 TestStructuredLogFormatterExceptions:
    """Test that StructuredLogFormatter includes exception tracebacks."""

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

        import sys

        try:
            raise ValueError("test error")
        except ValueError:
            exc_info = sys.exc_info()
            record = logger_instance.makeRecord(
                name="test.formatter",
                level=logging.ERROR,
                fn="test.py",
                lno=1,
                msg="Handler failed",
                args=(),
                exc_info=exc_info,
            )

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

        assert data["level"] == "ERROR"
        assert data["logger"] == "test.formatter"
        assert "Handler failed" in data["msg"]
        assert "ValueError: test error" in data["msg"]
        assert "Traceback (most recent call last):" in data["msg"]

    def test_no_exception_normal_msg(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["msg"] == "All good"
        assert "Traceback" not in data["msg"]


# =============================================================================
# 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_host_docker_internal_http_allowed(self):
        """HTTP host.docker.internal should be allowed for Docker 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_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_ipv6_localhost_http_allowed(self):
        """HTTP IPv6 localhost should be allowed."""
        sender = LogSender(nucleus_url="http://[::1]:8000", agent_api_key="test")
        assert sender.nucleus_url == "http://[::1]:8000"

    # -------------------------------------------------------------------------
    # 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")

    # -------------------------------------------------------------------------
    # Security: Bypass attempts (should raise ValueError)
    # -------------------------------------------------------------------------

    def test_query_string_bypass_rejected(self):
        """URL with localhost in query string should be rejected."""
        with pytest.raises(ValueError, match="must use HTTPS"):
            LogSender(nucleus_url="http://evil.com?redirect=://localhost", agent_api_key="test")

    def test_fragment_bypass_rejected(self):
        """URL with localhost in fragment should be rejected."""
        with pytest.raises(ValueError, match="must use HTTPS"):
            LogSender(nucleus_url="http://evil.com#://host.docker.internal", agent_api_key="test")

    def test_path_bypass_rejected(self):
        """URL with localhost in path should be rejected."""
        with pytest.raises(ValueError, match="must use HTTPS"):
            LogSender(nucleus_url="http://evil.com/://localhost/api", agent_api_key="test")

    def test_subdomain_spoof_localhost_rejected(self):
        """Subdomain spoofing localhost should be rejected."""
        with pytest.raises(ValueError, match="must use HTTPS"):
            LogSender(nucleus_url="http://localhost.evil.com:8000", agent_api_key="test")

    def test_subdomain_spoof_docker_internal_rejected(self):
        """Subdomain spoofing host.docker.internal should be rejected."""
        with pytest.raises(ValueError, match="must use HTTPS"):
            LogSender(nucleus_url="http://host.docker.internal.evil.com:5003", agent_api_key="test")

    def test_subdomain_spoof_k8s_rejected(self):
        """Subdomain spoofing svc.cluster.local should be rejected."""
        with pytest.raises(ValueError, match="must use HTTPS"):
            LogSender(nucleus_url="http://fake.svc.cluster.local.evil.com:8000", agent_api_key="test")


class TestIsDevelopmentUrl:
    """Unit tests for _is_development_url helper function."""

    def test_localhost_variations(self):
        """All localhost variations should be recognized."""
        assert _is_development_url("http://localhost:8000") is True
        assert _is_development_url("http://localhost") is True
        assert _is_development_url("https://localhost:443") is True
        assert _is_development_url("http://LOCALHOST:8000") is True  # Case insensitive

    def test_127_0_0_1_recognized(self):
        """127.0.0.1 should be recognized."""
        assert _is_development_url("http://127.0.0.1:5003") is True

    def test_ipv6_localhost_recognized(self):
        """IPv6 localhost should be recognized."""
        assert _is_development_url("http://[::1]:8000") is True

    def test_docker_internal_recognized(self):
        """host.docker.internal should be recognized."""
        assert _is_development_url("http://host.docker.internal:5003") is True
        assert _is_development_url("http://HOST.DOCKER.INTERNAL:5003") is True  # Case insensitive

    def test_k8s_internal_recognized(self):
        """Kubernetes internal services should be recognized."""
        assert _is_development_url("http://nucleus.default.svc.cluster.local") is True
        assert _is_development_url("http://my-service.my-namespace.svc.cluster.local:8080") is True

    def test_production_urls_not_recognized(self):
        """Production URLs should NOT be recognized as development."""
        assert _is_development_url("http://api.terminaluse.com") is False
        assert _is_development_url("https://api.terminaluse.com") is False
        assert _is_development_url("http://192.168.1.100:8000") is False

    def test_bypass_attempts_not_recognized(self):
        """Bypass attempts should NOT be recognized as development."""
        assert _is_development_url("http://evil.com?x=://localhost") is False
        assert _is_development_url("http://localhost.evil.com") is False
        assert _is_development_url("http://host.docker.internal.evil.com") is False

    def test_malformed_urls_return_false(self):
        """Malformed URLs should return False, not raise."""
        assert _is_development_url("not-a-url") is False
        assert _is_development_url("") is False
