"""Tests for the PerfMetrics widget and its integration with ChatPanel.

Tests cover:
- Initial state (hidden, zero values)
- start() resets counters and shows widget
- on_token() increments token count
- tokens/sec calculation
- stop() freezes values
- hide() resets state
- Integration with ChatPanel streaming lifecycle
"""

import time
from unittest.mock import patch

import pytest

# Skip all tests if textual not available
pytest.importorskip("textual")

from cleave.tui.widgets.perf_metrics import PerfMetrics


class TestPerfMetricsInitialState:
    """Tests for PerfMetrics widget initial state."""

    def test_perf_metrics_initial_state(self):
        """Widget should start hidden with zero values."""
        widget = PerfMetrics()
        assert widget._token_count == 0
        assert widget._start_time == 0.0
        assert widget._frozen == False
        assert widget._visible == False


class TestPerfMetricsStart:
    """Tests for PerfMetrics.start() method."""

    def test_perf_metrics_start_resets_and_shows(self):
        """Calling start() should reset counters and mark widget as visible."""
        widget = PerfMetrics()
        # Simulate prior state
        widget._token_count = 50
        widget._frozen = True
        widget._visible = False

        widget.start()

        assert widget._token_count == 0
        assert widget._start_time > 0.0
        assert widget._frozen == False
        assert widget._visible == True


    def test_perf_metrics_start_noop_when_already_streaming(self):
        """Calling start() while already streaming should not reset counters."""
        widget = PerfMetrics()
        widget.start()
        widget.on_token("a")
        widget.on_token("b")
        widget.on_token("c")
        token_count_before = widget._token_count
        start_time_before = widget._start_time

        # Second start() while visible and not frozen — should be a no-op
        widget.start()

        assert widget._token_count == token_count_before
        assert widget._start_time == start_time_before


class TestPerfMetricsOnToken:
    """Tests for PerfMetrics.on_token() method."""

    def test_perf_metrics_on_token_increments(self):
        """Each on_token() call should increment the token count."""
        widget = PerfMetrics()
        widget.start()

        widget.on_token("Hello")
        assert widget._token_count == 1

        widget.on_token(" world")
        assert widget._token_count == 2

        widget.on_token("!")
        assert widget._token_count == 3

    def test_perf_metrics_on_token_noop_when_frozen(self):
        """on_token() should not increment when widget is frozen (stopped)."""
        widget = PerfMetrics()
        widget.start()
        widget.on_token("a")
        widget.on_token("b")
        widget.stop()

        frozen_count = widget._token_count
        widget.on_token("c")  # Should be ignored
        assert widget._token_count == frozen_count


class TestPerfMetricsTokensPerSec:
    """Tests for tokens/sec calculation."""

    def test_perf_metrics_tokens_per_sec_calculation(self):
        """Token rate should be tokens divided by elapsed time."""
        widget = PerfMetrics()
        widget.start()

        # Fake the start time to be 2 seconds ago
        widget._start_time = time.perf_counter() - 2.0

        widget._token_count = 100  # Directly set for deterministic test

        rate = widget._get_tokens_per_sec()
        # Should be approximately 50 tok/s (100 tokens / 2 seconds)
        assert 45.0 <= rate <= 55.0

    def test_perf_metrics_tokens_per_sec_zero_when_no_time(self):
        """Rate should be 0 when no time has elapsed (avoid division by zero)."""
        widget = PerfMetrics()
        widget.start()
        # start_time is effectively "now", so elapsed is ~0
        # With 0 tokens, rate should be 0
        widget._token_count = 0

        rate = widget._get_tokens_per_sec()
        assert rate == 0.0


class TestPerfMetricsStop:
    """Tests for PerfMetrics.stop() method."""

    def test_perf_metrics_stop_freezes_values(self):
        """After stop(), values should not change on further on_token calls."""
        widget = PerfMetrics()
        widget.start()

        widget.on_token("a")
        widget.on_token("b")
        widget.on_token("c")

        widget.stop()
        assert widget._frozen == True

        frozen_count = widget._token_count
        frozen_elapsed = widget._get_elapsed()

        # Attempt to add more tokens
        widget.on_token("d")
        widget.on_token("e")

        # Values should remain frozen
        assert widget._token_count == frozen_count
        assert widget._get_elapsed() == frozen_elapsed


class TestPerfMetricsHide:
    """Tests for PerfMetrics.hide() method."""

    def test_perf_metrics_hide_clears_state(self):
        """hide() should reset all state and mark widget as not visible."""
        widget = PerfMetrics()
        widget.start()
        widget.on_token("x")
        widget.on_token("y")
        widget.stop()

        widget.hide()

        assert widget._token_count == 0
        assert widget._start_time == 0.0
        assert widget._frozen == False
        assert widget._visible == False


class TestPerfMetricsGetMemory:
    """Tests for memory reading utility."""

    def test_perf_metrics_get_memory_returns_number_or_none(self):
        """_get_memory_mb() should return a float (MB) or None if unavailable."""
        widget = PerfMetrics()
        result = widget._get_memory_mb()
        # On platforms with `resource` module, this should be a positive float
        # On platforms without it, None is acceptable
        assert result is None or (isinstance(result, float) and result > 0)


class TestPerfMetricsFormatDisplay:
    """Tests for display formatting."""

    def test_perf_metrics_format_display_while_streaming(self):
        """_format_display() should produce a non-empty string while streaming."""
        widget = PerfMetrics()
        widget.start()
        widget.on_token("Hello")
        widget.on_token(" world")

        display = widget._format_display()
        assert len(display) > 0
        # Should contain latency, token count indicators
        assert "tok" in display.lower() or "2" in display

    def test_perf_metrics_format_display_includes_memory(self):
        """Display should include memory info (or N/A placeholder)."""
        widget = PerfMetrics()
        widget.start()
        widget.on_token("x")

        display = widget._format_display()
        # Should contain either a memory value (RSS label) or N/A
        assert "RSS" in display or "N/A" in display


class TestPerfMetricsIntegrationWithChatPanel:
    """Integration tests for PerfMetrics with ChatPanel."""

    def test_chat_panel_has_perf_metrics(self):
        """ChatPanel should expose a perf_metrics property."""
        from cleave.tui.widgets.chat_panel import ChatPanel

        panel = ChatPanel()
        assert hasattr(panel, "_perf_metrics")


@pytest.mark.asyncio
class TestPerfMetricsStreamingIntegration:
    """Async integration tests with the running TUI."""

    async def test_streaming_shows_perf_metrics(self):
        """Starting streaming on ChatPanel should show PerfMetrics with tokens."""
        import tempfile
        from pathlib import Path

        from cleave.tui.app import CleaveTUI
        from cleave.tui.widgets.chat_panel import ChatPanel

        with tempfile.TemporaryDirectory() as tmpdir:
            app = CleaveTUI(working_directory=Path(tmpdir))
            async with app.run_test() as pilot:
                await pilot.pause()
                chat = app.screen.query_one("#chat-widget", ChatPanel)

                # Start streaming and feed tokens
                chat.start_streaming("assistant")
                chat.add_streaming_token("Hello")
                chat.add_streaming_token(" there")
                chat.add_streaming_token("!")
                await pilot.pause()

                # PerfMetrics should be visible and have token count
                perf = chat._perf_metrics
                assert perf._visible == True
                assert perf._token_count == 3

                # Finalize
                chat.finalize_streaming()
                await pilot.pause()

                # After finalize, frozen but still visible
                assert perf._frozen == True

    async def test_perf_metrics_hidden_after_new_submission(self):
        """After streaming completes and user submits again, metrics should hide."""
        import tempfile
        from pathlib import Path

        from cleave.tui.app import CleaveTUI
        from cleave.tui.widgets.chat_panel import ChatPanel

        with tempfile.TemporaryDirectory() as tmpdir:
            app = CleaveTUI(working_directory=Path(tmpdir))
            async with app.run_test() as pilot:
                await pilot.pause()
                chat = app.screen.query_one("#chat-widget", ChatPanel)

                # Complete a streaming cycle
                chat.start_streaming("assistant")
                chat.add_streaming_token("Response")
                chat.finalize_streaming()
                await pilot.pause()

                # Metrics should be frozen and visible
                assert chat._perf_metrics._frozen == True

                # Simulate new submission via internal handler
                chat._handle_submit("new question")
                await pilot.pause()

                # Metrics should now be hidden
                assert chat._perf_metrics._visible == False
