"""Integration tests for session compression (Layer 0 and Layer 1).

Spec Reference: RFC-002-R1
- §3.1: Layer 0 Compression Rules
- §3.2: Layer 1 Summarization
- §4.1: Event Types

These tests verify integration between:
- Core types (Event, EventType, estimate_tokens, should_summarize)
- Shell layer (layer1_summarize)
- Config types (Layer1Config)

Layer 0: Event ingestion with compression (truncate_error, summarize_success_output, etc.)
Layer 1: Ollama-based summarization (should_summarize, layer1_summarize)
"""

import pytest
from returns.result import Failure, Success

from lattice.core.types.config import Layer1Config
from lattice.core.types.enums import EventType
from lattice.core.types.event import (
    Event,
    estimate_tokens,
    is_high_signal,
    should_summarize,
    summarize_error_output,
    summarize_success_output,
    summarize_tool_input,
    truncate_error,
)
from lattice.shell.layer1 import layer1_summarize


# =============================================================================
# Layer 0 Compression Tests
# =============================================================================


class TestLayer0EventIngestion:
    """Test Layer 0 event creation and compression metadata."""

    def test_user_event_stores_full_content(self) -> None:
        """User events store full content without compression."""
        event = Event(
            type=EventType.USER,
            content="This is a detailed user question about authentication flows.",
        )
        assert event.type == EventType.USER
        assert "authentication" in event.content
        assert event.tool_input == ""  # No tool fields for user events
        assert event.tool_status == ""
        assert event.tool_error == ""

    def test_tool_event_stores_metadata_only(self) -> None:
        """Tool events store metadata (input, status), not raw output."""
        event = Event(
            type=EventType.TOOL,
            content="read",
            tool_input="src/auth.py",
            tool_status="success",
        )
        # Raw output is NOT stored in the event (compression)
        assert event.content == "read"
        assert event.tool_input == "src/auth.py"
        assert event.tool_status == "success"
        assert event.tool_error == ""

    def test_tool_error_event_stores_truncated_error(self) -> None:
        """Tool error events store truncated error message."""
        long_error = "Error: " + "x" * 1000
        truncated = truncate_error(long_error, max_len=500)

        event = Event(
            type=EventType.TOOL,
            content="edit",
            tool_input="src/auth.py",
            tool_status="error",
            tool_error=truncated,
        )

        assert event.tool_status == "error"
        assert len(event.tool_error) < len(long_error)
        assert "truncated" in event.tool_error.lower()

    def test_reasoning_event_preserved_in_full(self) -> None:
        """Reasoning events (high-signal) are preserved in full."""
        reasoning = "Analyzing the codebase structure... " * 100  # Long reasoning
        event = Event(type=EventType.REASONING, content=reasoning)

        assert is_high_signal(event) is True
        assert event.content == reasoning  # Full content preserved

    def test_assistant_event_preserved_in_full(self) -> None:
        """Assistant events (high-signal) are preserved in full."""
        response = (
            "I'll implement the authentication module with the following approach..."
        )
        event = Event(type=EventType.ASSISTANT, content=response)

        assert is_high_signal(event) is True
        assert event.content == response


class TestLayer0ErrorTruncation:
    """Test error message truncation (truncate_error contract)."""

    def test_short_error_preserved_unchanged(self) -> None:
        """Short errors (< max_len) are preserved unchanged."""
        error = "SyntaxError: invalid syntax on line 42"
        result = truncate_error(error)
        assert result == error

    def test_long_error_truncated_with_marker(self) -> None:
        """Long errors are truncated with a marker showing original length."""
        error = "x" * 1000
        result = truncate_error(error, max_len=100)

        assert len(result) < len(error)
        assert "truncated" in result.lower()
        assert "1000" in result  # Original length visible

    def test_empty_error_valid(self) -> None:
        """Empty string is valid (represents 'no error')."""
        result = truncate_error("")
        assert result == ""

    def test_custom_max_len_respected(self) -> None:
        """Custom max_len parameter is respected."""
        error = "y" * 500
        result = truncate_error(error, max_len=50)

        # Result should be truncated
        assert len(result) < len(error)
        # Should include original length
        assert "500" in result


class TestLayer0ToolInputSummarization:
    """Test tool input summarization (summarize_tool_input contract)."""

    def test_read_tool_keeps_file_path(self) -> None:
        """read tool input summarizes to file path."""
        result = summarize_tool_input("read", {"file_path": "src/auth.py"})
        assert result == "src/auth.py"

    def test_bash_tool_keeps_command(self) -> None:
        """bash tool input summarizes to command."""
        result = summarize_tool_input("bash", {"command": "npm test -- --coverage"})
        assert result == "npm test -- --coverage"

    def test_edit_tool_keeps_file_path(self) -> None:
        """edit tool input summarizes to file path (ignores old_string/new_string)."""
        result = summarize_tool_input(
            "edit",
            {
                "file_path": "src/auth.py",
                "old_string": "def auth(): pass",
                "new_string": "def auth(): return True",
            },
        )
        assert result == "src/auth.py"

    def test_glob_tool_keeps_pattern(self) -> None:
        """glob tool input summarizes to pattern."""
        result = summarize_tool_input("glob", {"pattern": "**/*.py"})
        assert result == "**/*.py"

    def test_write_tool_keeps_file_path(self) -> None:
        """write tool input summarizes to file path."""
        result = summarize_tool_input("write", {"file_path": "output.txt"})
        assert result == "output.txt"

    def test_unknown_tool_generic_fallback(self) -> None:
        """Unknown tools use generic fallback (try common fields)."""
        result = summarize_tool_input("custom_tool", {"url": "https://api.example.com"})
        assert result == "https://api.example.com"

    def test_none_input_returns_empty(self) -> None:
        """None input returns empty string."""
        result = summarize_tool_input("read", None)
        assert result == ""

    def test_string_input_truncated_to_100(self) -> None:
        """String input is truncated to 100 chars."""
        long_string = "x" * 500
        result = summarize_tool_input("custom", long_string)
        assert len(result) <= 100

    def test_very_long_command_truncated(self) -> None:
        """Very long commands are truncated to 100 chars.

        Contract: @post lambda result: len(result) <= 100
        """
        long_cmd = {
            "command": "npm install " + " ".join([f"package{i}" for i in range(50)])
        }
        result = summarize_tool_input("bash", long_cmd)
        assert len(result) <= 100


class TestLayer0OutputSummarization:
    """Test tool output summarization."""

    def test_success_output_keeps_size_only(self) -> None:
        """Success output summarizes to size metadata only.

        Contract: @post lambda result: len(result) <= 50
        """
        large_output = "x" * 10000
        result = summarize_success_output(large_output)

        # Large output includes token estimate
        assert "10000 chars" in result
        assert len(result) <= 50

    def test_success_output_bytes_format(self) -> None:
        """Bytes output shows byte count."""
        output = b"hello world"
        result = summarize_success_output(output)
        assert "[11 bytes]" in result

    def test_success_output_large_includes_tokens(self) -> None:
        """Large output includes token estimate (~4 chars/token)."""
        large_output = "x" * 10000  # ~2500 tokens
        result = summarize_success_output(large_output)
        assert "2500 tokens" in result

    def test_error_output_keeps_first_n_chars(self) -> None:
        """Error output keeps first max_len chars."""
        error = "ImportError: No module named 'jwt'"
        result = summarize_error_output(error)
        assert error in result  # Short error preserved

    def test_error_output_truncates_long_messages(self) -> None:
        """Long error output is truncated."""
        error = "Error: " + "x" * 1000
        result = summarize_error_output(error, max_len=100)
        assert len(result) < len(error)
        assert "truncated" in result.lower()


# =============================================================================
# Layer 1 Summarization Tests
# =============================================================================


class TestLayer1TokenEstimation:
    """Test token estimation for summarization decisions."""

    def test_estimate_tokens_empty_events(self) -> None:
        """Empty events list returns 0 tokens."""
        tokens = estimate_tokens([])
        assert tokens == 0

    def test_estimate_tokens_simple_event(self) -> None:
        """Simple event estimation uses ~4 chars per token."""
        event = Event(type=EventType.USER, content="Hello world")  # 11 chars
        tokens = estimate_tokens([event])

        # 11 chars + 20 overhead = 31 chars total ÷ 4 = ~7 tokens
        assert tokens >= 7  # At least content-based estimate
        assert isinstance(tokens, int)

    def test_estimate_tokens_includes_tool_metadata(self) -> None:
        """Token estimation includes tool event metadata."""
        events = [
            Event(
                type=EventType.TOOL,
                content="read",
                tool_input="src/auth.py",
                tool_status="success",
            ),
        ]
        tokens = estimate_tokens(events)

        # Includes: content("read") + input("src/auth.py") + overhead
        assert tokens > 0
        assert isinstance(tokens, int)

    def test_estimate_tokens_includes_error_message(self) -> None:
        """Token estimation includes tool error messages."""
        events = [
            Event(
                type=EventType.TOOL,
                content="edit",
                tool_input="file.py",
                tool_status="error",
                tool_error="SyntaxError: invalid syntax on line 42",
            ),
        ]
        tokens_with_error = estimate_tokens(events)

        events_no_error = [
            Event(
                type=EventType.TOOL,
                content="edit",
                tool_input="file.py",
                tool_status="success",
            ),
        ]
        tokens_no_error = estimate_tokens(events_no_error)

        # Events with error should have more tokens
        assert tokens_with_error > tokens_no_error

    def test_estimate_tokens_multiple_events(self) -> None:
        """Multiple events accumulate token counts."""
        events = [
            Event(type=EventType.USER, content="Question 12345"),  # 14 chars
            Event(type=EventType.ASSISTANT, content="Answer 12345"),  # 12 chars
        ]
        tokens = estimate_tokens(events)

        # Both events contribute: (14 + 12 + 20*2) / 4 = 16.5 → 16 tokens
        assert tokens > 0


class TestLayer1ShouldSummarize:
    """Test should_summarize decision logic."""

    def test_disabled_config_returns_false(self) -> None:
        """Disabled Layer1 config returns False."""
        config = Layer1Config(enabled=False)
        events = [Event(type=EventType.USER, content="x" * 10000)]

        result = should_summarize(events, config)
        assert result is False

    def test_enabled_below_threshold_returns_false(self) -> None:
        """Enabled Layer1 below min_tokens returns False."""
        config = Layer1Config(enabled=True, min_tokens=10000)
        events = [Event(type=EventType.USER, content="short")]  # ~23 chars → ~5 tokens

        result = should_summarize(events, config)
        assert result is False

    def test_enabled_above_threshold_returns_true(self) -> None:
        """Enabled Layer1 above min_tokens returns True."""
        config = Layer1Config(enabled=True, min_tokens=10)
        events = [
            Event(type=EventType.USER, content="x" * 100)
        ]  # ~120 chars → ~30 tokens

        result = should_summarize(events, config)
        assert result is True

    def test_default_min_tokens_one_million(self) -> None:
        """Default Layer1Config has min_tokens=1,000,000."""
        config = Layer1Config()
        assert config.min_tokens == 1_000_000

    def test_custom_min_tokens_threshold(self) -> None:
        """Custom min_tokens threshold is respected."""
        config = Layer1Config(enabled=True, min_tokens=100)
        events = [
            Event(type=EventType.USER, content="x" * 500)
        ]  # ~520 chars → ~130 tokens

        result = should_summarize(events, config)
        assert result is True

    def test_empty_events_never_summarize(self) -> None:
        """Empty events list should not trigger summarization."""
        config = Layer1Config(enabled=True, min_tokens=0)
        events: list[Event] = []

        result = should_summarize(events, config)
        # Even with min_tokens=0, need actual content to summarize
        # estimate_tokens([]) = 0, so 0 >= 0 = True... let's check
        # Actually, if min_tokens=0 and tokens=0, 0 >= 0 is True!
        # But that's edge case - typically min_tokens > 0
        tokens = estimate_tokens(events)
        assert tokens == 0


class TestLayer1SummarizationFunction:
    """Test layer1_summarize shell function."""

    def test_disabled_returns_failure(self) -> None:
        """Disabled config returns Failure immediately."""
        config = Layer1Config(enabled=False)
        events = [Event(type=EventType.USER, content="Hello")]

        result = layer1_summarize(events, config)

        assert isinstance(result, Failure)
        assert "disabled" in result.failure().lower()

    def test_enabled_returns_failure_without_ollama(self) -> None:
        """Enabled config fails gracefully when Ollama not available."""
        config = Layer1Config(enabled=True, ollama_host="http://nonexistent:11434")
        events = [
            Event(type=EventType.USER, content="Test message"),
        ]

        result = layer1_summarize(events, config)

        # Should be a Failure (connection error, timeout, or ImportError)
        assert isinstance(result, Failure)
        # Error message should indicate the problem
        error_msg = result.failure()
        assert isinstance(error_msg, str)
        assert len(error_msg) > 0

    def test_formats_events_correctly(self) -> None:
        """Events are formatted for the summarization prompt.

        This is a contract test - we verify the function accepts proper input.
        """
        config = Layer1Config(enabled=True)
        events = [
            Event(type=EventType.USER, content="Fix the bug in auth.py"),
            Event(
                type=EventType.TOOL,
                content="read",
                tool_input="src/auth.py",
                tool_status="success",
            ),
            Event(
                type=EventType.TOOL,
                content="edit",
                tool_input="src/auth.py",
                tool_status="error",
                tool_error="SyntaxError on line 42",
            ),
            Event(type=EventType.ASSISTANT, content="I fixed the syntax error."),
        ]

        # The function should accept these events without error
        # (it will fail due to no Ollama, but input validation should pass)
        result = layer1_summarize(events, config)

        # Result should be Failure (no Ollama), but not an input validation error
        assert isinstance(result, Failure)


# =============================================================================
# End-to-End Compression Flow Tests
# =============================================================================


class TestEndToEndCompressionFlow:
    """Test full compression pipeline from events to decisions."""

    def test_layer0_compression_reduces_size(self) -> None:
        """Layer 0 compression significantly reduces stored data size."""
        # Simulate a typical session
        events = [
            Event(type=EventType.USER, content="Show me the authentication code"),
            Event(type=EventType.REASONING, content="Need to find auth module..."),
            Event(
                type=EventType.TOOL,
                content="read",
                tool_input="src/auth.py",
                tool_status="success",
                # Note: raw output (e.g., 50KB file content) NOT stored
            ),
            Event(
                type=EventType.TOOL,
                content="bash",
                tool_input="npm test",
                tool_status="success",
                # Note: raw output (e.g., test results) NOT stored
            ),
            Event(
                type=EventType.ASSISTANT,
                content="The authentication module uses JWT tokens with RS256 signing.",
            ),
        ]

        # Calculate what we store
        stored_chars = sum(
            len(e.content) + len(e.tool_input) + len(e.tool_error) for e in events
        )

        # Simulated raw size (if we stored full tool outputs)
        raw_output_size = 50000 + 5000  # file content + test output

        # Compression ratio: stored size is much smaller than raw
        # actual compression ratio = raw_output_size / stored_chars
        assert stored_chars < raw_output_size  # Our stored data is smaller

    def test_layer1_decision_integration(self) -> None:
        """Layer 1 decision uses estimate_tokens correctly."""
        # Large session that would trigger Layer 1
        large_events = [
            Event(type=EventType.USER, content="x" * 50000)  # ~12500 tokens
        ]

        config_low_threshold = Layer1Config(enabled=True, min_tokens=10000)
        config_high_threshold = Layer1Config(enabled=True, min_tokens=20000)

        # Should summarize with low threshold
        assert should_summarize(large_events, config_low_threshold) is True

        # Should not summarize with high threshold
        assert should_summarize(large_events, config_high_threshold) is False

    def test_compression_preserves_high_signal_content(self) -> None:
        """High-signal content (user, assistant, reasoning) is preserved."""
        user_msg = "I need help with the authentication system"
        reasoning_msg = "Analyzing the JWT implementation..."
        assistant_msg = "I found the issue in the token validation."

        events = [
            Event(type=EventType.USER, content=user_msg),
            Event(type=EventType.REASONING, content=reasoning_msg),
            Event(type=EventType.ASSISTANT, content=assistant_msg),
        ]

        # All high-signal events preserved in full
        for event in events:
            assert is_high_signal(event) is True

        # Content is unchanged
        assert events[0].content == user_msg
        assert events[1].content == reasoning_msg
        assert events[2].content == assistant_msg

    def test_tool_events_only_store_metadata(self) -> None:
        """Tool events store only metadata, not raw output."""
        # Read tool with large output
        read_event = Event(
            type=EventType.TOOL,
            content="read",
            tool_input="src/auth.py",
            tool_status="success",
        )

        # No raw_output field exists on Event
        assert not hasattr(read_event, "raw_output")

        # Only metadata is stored
        assert read_event.content == "read"
        assert read_event.tool_input == "src/auth.py"
        assert read_event.tool_status == "success"

        # Error tool stores truncated error
        error_event = Event(
            type=EventType.TOOL,
            content="edit",
            tool_input="src/auth.py",
            tool_status="error",
            tool_error="SyntaxError: invalid syntax",
        )

        assert error_event.tool_status == "error"
        assert len(error_event.tool_error) <= 500  # Default max error length

    def test_complete_session_compression_flow(self) -> None:
        """Complete flow: raw events → Layer 0 compression → Layer 1 decision."""
        # Simulate a complete session
        events = [
            # User request
            Event(type=EventType.USER, content="Fix the authentication bug"),
            # Agent reasoning
            Event(
                type=EventType.REASONING, content="Need to check the JWT validation..."
            ),
            # Tool use (large output compressed)
            Event(
                type=EventType.TOOL,
                content="read",
                tool_input="src/auth.py",
                tool_status="success",
            ),
            # Tool error
            Event(
                type=EventType.TOOL,
                content="edit",
                tool_input="src/auth.py",
                tool_status="error",
                tool_error=truncate_error(
                    "SyntaxError on line 42: unexpected token 'def'"
                ),
            ),
            # Agent response
            Event(
                type=EventType.ASSISTANT,
                content="I fixed the syntax error. The issue was a missing colon.",
            ),
        ]

        # Verify Layer 0 compression worked
        for event in events:
            if event.type == EventType.TOOL:
                # Tool events are low-signal
                assert is_high_signal(event) is False
                # No raw output stored
                assert not hasattr(event, "raw_output")
            else:
                # Other events are high-signal
                assert is_high_signal(event) is True

        # Verify Layer 1 decision
        tokens = estimate_tokens(events)
        assert tokens > 0

        # With default min_tokens (1M), should not summarize typical sessions
        default_config = Layer1Config()
        assert should_summarize(events, default_config) is False

        # With low threshold, would summarize
        low_threshold_config = Layer1Config(enabled=True, min_tokens=10)
        assert should_summarize(events, low_threshold_config) is True


class TestLayer0Layer1Integration:
    """Test integration between Layer 0 and Layer 1."""

    def test_compression_before_summarization(self) -> None:
        """Layer 0 compression happens before Layer 1 decision."""
        # Create events that would be large without compression
        events = [
            Event(type=EventType.USER, content="x" * 1000),
            # Tool event - output was compressed, only metadata stored
            Event(
                type=EventType.TOOL,
                content="read",
                tool_input="file.py",
                tool_status="success",
            ),
        ]

        # Token estimation uses compressed data
        tokens = estimate_tokens(events)

        # Without compression, a 50KB file would add ~12,500 tokens
        # With compression, only metadata adds ~30 tokens
        # So estimate_tokens should be relatively small
        assert tokens < 1000  # Much less than raw file size would imply

    def test_error_truncation_before_storage(self) -> None:
        """Errors are truncated before being stored in events."""
        huge_error = "Error: " + "x" * 10000

        # Truncate first (this would happen during event creation)
        truncated = truncate_error(huge_error, max_len=500)

        # Then store in event
        event = Event(
            type=EventType.TOOL,
            content="bash",
            tool_input="npm run build",
            tool_status="error",
            tool_error=truncated,
        )

        # Verify truncation happened
        assert len(event.tool_error) < len(huge_error)
        assert "truncated" in event.tool_error.lower()

        # Token estimation should be reasonable
        tokens = estimate_tokens([event])
        assert tokens < 1000  # Not dominated by huge error

    def test_tool_input_summarization_before_storage(self) -> None:
        """Tool inputs are summarized before being stored in events."""
        # Large tool input
        large_input = {
            "command": "npm install " + " ".join([f"package{i}" for i in range(100)])
        }

        # Summarize first
        summary = summarize_tool_input("bash", large_input)

        # Then store in event
        event = Event(
            type=EventType.TOOL,
            content="bash",
            tool_input=summary,
            tool_status="success",
        )

        # Verify summarization happened
        assert len(event.tool_input) <= 100
        assert "npm install" in event.tool_input


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
