"""Tests for the Signals screen: table rendering, filter, refresh, empty state."""

import httpx
import pytest
from unittest.mock import MagicMock, patch

from textual.widgets import DataTable, Input, Static

from skilark_cli.tui.app import SkilarkApp
from skilark_cli.tui.screens.signals import SignalsPane


SAMPLE_SIGNALS = {
    "week_of": "2026-03-02",
    "signals": [
        {
            "signal_type": "company_surge",
            "title": "Stripe hiring surge",
            "body": "Stripe posted 15 new ML roles this week, 3x previous week.",
            "src_timestamp": "2026-03-02T10:00:00Z",
        },
        {
            "signal_type": "company_surge",
            "title": "Anthropic hiring surge",
            "body": "Anthropic posted 23 new backend roles, 2x previous week.",
            "src_timestamp": "2026-03-02T12:00:00Z",
        },
        {
            "signal_type": "salary_insight",
            "title": "Kubernetes salary premium",
            "body": "Kubernetes roles pay $185K median, 15% above market.",
            "src_timestamp": "2026-03-02T14:00:00Z",
        },
        {
            "type": "company_news",
            "title": "Amazon: Amazon cuts jobs in robotics unit",
            "body": "Source: WSJ. Keywords: layoff.",
            "src_timestamp": "2026-03-02T08:00:00Z",
            "metadata": {
                "source_name": "WSJ",
                "news_type": "layoff",
                "published_at": "2026-03-02T08:00:00Z",
                "article_url": "https://example.com/article",
                "company_name": "Amazon",
            },
        },
    ],
}


def _mock_client(data=None, error=None):
    client = MagicMock()
    if error:
        client.get_signals.side_effect = error
    else:
        client.get_signals.return_value = data or SAMPLE_SIGNALS
    # Drill tab needs these on app launch (default tab is now Drill)
    client.get_next_challenge.return_value = None
    client.get_stats.return_value = {"total_completed": 0}
    return client


@pytest.fixture
def app_with_signals():
    client = _mock_client()
    # Start on Signals tab (index 1) so signals widgets are mounted
    app = SkilarkApp(client=client)
    app._active_tab = 1
    return app


@pytest.fixture
def app_empty():
    client = _mock_client(data={"week_of": "2026-03-02", "signals": []})
    app = SkilarkApp(client=client)
    app._active_tab = 1
    return app


@pytest.fixture
def app_error():
    client = _mock_client(error=Exception("Connection refused"))
    app = SkilarkApp(client=client)
    app._active_tab = 1
    return app


class TestSignalsRendering:
    async def test_signals_table_has_rows(self, app_with_signals):
        async with app_with_signals.run_test() as pilot:
            await pilot.pause(delay=0.2)
            table = app_with_signals.query_one("#signals-table", DataTable)
            assert table.row_count == 4

    async def test_header_shows_week(self, app_with_signals):
        async with app_with_signals.run_test() as pilot:
            await pilot.pause(delay=0.2)
            header = app_with_signals.query_one("#signals-header", Static)
            assert "Mar 2" in header.content

    async def test_header_shows_count(self, app_with_signals):
        async with app_with_signals.run_test() as pilot:
            await pilot.pause(delay=0.2)
            header = app_with_signals.query_one("#signals-header", Static)
            assert "4 signals" in header.content

    async def test_empty_signals(self, app_empty):
        async with app_empty.run_test() as pilot:
            await pilot.pause(delay=0.2)
            table = app_empty.query_one("#signals-table", DataTable)
            assert table.row_count == 0

    async def test_api_error(self, app_error):
        async with app_error.run_test() as pilot:
            await pilot.pause(delay=0.2)
            header = app_error.query_one("#signals-header", Static)
            assert "Something unexpected happened" in header.content


class TestSignalsNewsDetail:
    async def test_news_detail_shows_source(self, app_with_signals):
        async with app_with_signals.run_test() as pilot:
            await pilot.pause(delay=0.2)
            table = app_with_signals.query_one("#signals-table", DataTable)
            # Select the news signal (last row, index 3)
            table.move_cursor(row=3)
            await pilot.pause()
            detail = app_with_signals.query_one("#signals-detail", Static)
            assert "WSJ" in detail.content

    async def test_news_detail_shows_link(self, app_with_signals):
        async with app_with_signals.run_test() as pilot:
            await pilot.pause(delay=0.2)
            table = app_with_signals.query_one("#signals-table", DataTable)
            table.move_cursor(row=3)
            await pilot.pause()
            detail = app_with_signals.query_one("#signals-detail", Static)
            assert "example.com/article" in detail.content

    async def test_news_detail_shows_news_type(self, app_with_signals):
        async with app_with_signals.run_test() as pilot:
            await pilot.pause(delay=0.2)
            table = app_with_signals.query_one("#signals-table", DataTable)
            table.move_cursor(row=3)
            await pilot.pause()
            detail = app_with_signals.query_one("#signals-detail", Static)
            assert "Layoff" in detail.content

    async def test_table_shows_source_column(self, app_with_signals):
        async with app_with_signals.run_test() as pilot:
            await pilot.pause(delay=0.2)
            table = app_with_signals.query_one("#signals-table", DataTable)
            col_labels = [str(c.label) for c in table.columns.values()]
            assert col_labels == ["Type", "Time", "Subject", "Source"]


class TestSignalsOpenLink:
    async def test_open_link_calls_webbrowser(self, app_with_signals):
        async with app_with_signals.run_test() as pilot:
            await pilot.pause(delay=0.2)
            table = app_with_signals.query_one("#signals-table", DataTable)
            # Select the news signal (last row, index 3)
            table.move_cursor(row=3)
            await pilot.pause()
            with patch("skilark_cli.tui.screens.signals.webbrowser") as mock_wb:
                await pilot.press("o")
                await pilot.pause()
                mock_wb.open.assert_called_once_with("https://example.com/article")

    async def test_open_link_noop_for_non_news(self, app_with_signals):
        async with app_with_signals.run_test() as pilot:
            await pilot.pause(delay=0.2)
            table = app_with_signals.query_one("#signals-table", DataTable)
            # Select a non-news signal (first row)
            table.move_cursor(row=0)
            await pilot.pause()
            with patch("skilark_cli.tui.screens.signals.webbrowser") as mock_wb:
                await pilot.press("o")
                await pilot.pause()
                mock_wb.open.assert_not_called()


class TestSignalsFilter:
    async def test_filter_input_hidden_by_default(self, app_with_signals):
        async with app_with_signals.run_test() as pilot:
            await pilot.pause(delay=0.2)
            filter_input = app_with_signals.query_one("#filter-input", Input)
            assert not filter_input.display

    async def test_slash_shows_filter(self, app_with_signals):
        async with app_with_signals.run_test() as pilot:
            await pilot.pause(delay=0.2)
            await pilot.press("slash")
            filter_input = app_with_signals.query_one("#filter-input", Input)
            assert filter_input.display
            assert filter_input.has_focus

    async def test_filter_reduces_rows(self, app_with_signals):
        async with app_with_signals.run_test() as pilot:
            await pilot.pause(delay=0.2)
            await pilot.press("slash")
            await pilot.pause()
            # Type filter text via keystrokes
            for char in "stripe":
                await pilot.press(char)
            await pilot.pause()
            table = app_with_signals.query_one("#signals-table", DataTable)
            assert table.row_count == 1

    async def test_escape_clears_filter(self, app_with_signals):
        async with app_with_signals.run_test() as pilot:
            await pilot.pause(delay=0.2)
            await pilot.press("slash")
            await pilot.pause()
            for char in "stripe":
                await pilot.press(char)
            await pilot.pause()
            table = app_with_signals.query_one("#signals-table", DataTable)
            assert table.row_count == 1
            await pilot.press("escape")
            await pilot.pause()
            assert table.row_count == 4


class TestSignalsRateLimit:
    async def test_rate_limit_shows_friendly_message(self):
        response = httpx.Response(429, json={"error": "rate limit exceeded"})
        exc = httpx.HTTPStatusError(
            "", request=httpx.Request("GET", "http://x"), response=response
        )
        client = _mock_client(error=exc)
        app = SkilarkApp(client=client)
        app._active_tab = 1
        async with app.run_test() as pilot:
            await pilot.pause(delay=0.2)
            header = app.query_one("#signals-header", Static)
            assert "Slow down" in header.content
            assert "429" not in header.content


class TestSignalsSortOrder:
    """Signals should be sorted newest-first by timestamp."""

    async def test_signals_sorted_newest_first(self):
        """Signals arrive in API order but display newest first."""
        data = {
            "week_of": "2026-03-02",
            "signals": [
                {"type": "company_surge", "title": "Old signal", "body": "",
                 "src_timestamp": "2026-03-01T08:00:00Z"},
                {"type": "salary_insight", "title": "Newest signal", "body": "",
                 "src_timestamp": "2026-03-02T18:00:00Z"},
                {"type": "company_news", "title": "Mid signal", "body": "",
                 "src_timestamp": "2026-03-02T10:00:00Z",
                 "metadata": {"news_type": "hiring", "published_at": "2026-03-02T10:00:00Z"}},
            ],
        }
        client = _mock_client(data=data)
        app = SkilarkApp(client=client)
        app._active_tab = 1
        async with app.run_test() as pilot:
            await pilot.pause(delay=0.2)
            pane = app.query_one(SignalsPane)
            assert pane._signals[0]["title"] == "Newest signal"
            assert pane._signals[1]["title"] == "Mid signal"
            assert pane._signals[2]["title"] == "Old signal"

    async def test_signals_without_timestamp_sort_last(self):
        """Signals missing src_timestamp sort to the end."""
        data = {
            "week_of": "2026-03-02",
            "signals": [
                {"type": "company_surge", "title": "No timestamp", "body": ""},
                {"type": "salary_insight", "title": "Has timestamp", "body": "",
                 "src_timestamp": "2026-03-02T10:00:00Z"},
            ],
        }
        client = _mock_client(data=data)
        app = SkilarkApp(client=client)
        app._active_tab = 1
        async with app.run_test() as pilot:
            await pilot.pause(delay=0.2)
            pane = app.query_one(SignalsPane)
            assert pane._signals[0]["title"] == "Has timestamp"
            assert pane._signals[1]["title"] == "No timestamp"

    def test_signal_timestamp_extraction(self):
        """_signal_timestamp extracts from src_timestamp or metadata.published_at."""
        assert SignalsPane._signal_timestamp(
            {"src_timestamp": "2026-03-02T10:00:00Z"}
        ) == "2026-03-02T10:00:00Z"
        assert SignalsPane._signal_timestamp(
            {"metadata": {"published_at": "2026-03-01T12:00:00Z"}}
        ) == "2026-03-01T12:00:00Z"
        assert SignalsPane._signal_timestamp({}) == ""
        assert SignalsPane._signal_timestamp({"metadata": "not a dict"}) == ""


class TestSignalsRefresh:
    async def test_refresh_refetches(self, app_with_signals):
        async with app_with_signals.run_test() as pilot:
            await pilot.pause(delay=0.2)
            await pilot.press("r")
            await pilot.pause(delay=0.2)
            assert app_with_signals.client.get_signals.call_count == 2

    async def test_refresh_shows_loading(self, app_with_signals):
        async with app_with_signals.run_test() as pilot:
            await pilot.pause(delay=0.2)
            # Make the next call slow so we can see "Loading..."
            import threading

            barrier = threading.Event()
            original_fn = app_with_signals.client.get_signals.return_value

            def slow_fetch(*args, **kwargs):
                barrier.wait(timeout=1)
                return SAMPLE_SIGNALS

            app_with_signals.client.get_signals.side_effect = slow_fetch
            await pilot.press("r")
            await pilot.pause()
            header = app_with_signals.query_one("#signals-header", Static)
            assert "Loading" in header.content
            barrier.set()
            await pilot.pause(delay=0.2)
