"""
Tests for brynq.search — Cross-channel message search with filtering.
"""

import json
from datetime import datetime, timezone

import pytest


@pytest.fixture(autouse=True)
def _isolate_search(broker_dirs, monkeypatch):
    """Redirect search module's CHANNELS_DIR to the temp broker."""
    import brynq.search as search_mod
    monkeypatch.setattr(search_mod, "CHANNELS_DIR", broker_dirs["channels"])


def _write_msg(channels_dir, channel, msg, **overrides):
    """Helper to write a message to a channel JSONL file."""
    entry = {
        "id": "test-id",
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "session_id": "test-session",
        "session_name": "TestBot",
        "platform": "cli",
        "channel": channel,
        "msg_type": "info",
        "message": msg,
    }
    entry.update(overrides)
    path = channels_dir / f"{channel}.jsonl"
    with path.open("a", encoding="utf-8") as f:
        f.write(json.dumps(entry, default=str) + "\n")
    return entry


# ── Basic Search ─────────────────────────────────────────────────────────


class TestSearchBasic:
    def test_search_by_query(self, broker_dirs):
        from brynq.search import search
        _write_msg(broker_dirs["channels"], "dev", "backfill completed")
        _write_msg(broker_dirs["channels"], "dev", "tests passing")
        results = search(query="backfill")
        assert len(results) == 1
        assert "backfill" in results[0]["message"]

    def test_search_case_insensitive(self, broker_dirs):
        from brynq.search import search
        _write_msg(broker_dirs["channels"], "dev", "Backfill DONE")
        results = search(query="backfill")
        assert len(results) == 1

    def test_search_no_match(self, broker_dirs):
        from brynq.search import search
        _write_msg(broker_dirs["channels"], "dev", "hello world")
        results = search(query="nonexistent")
        assert len(results) == 0

    def test_search_returns_newest_first(self, broker_dirs):
        from brynq.search import search
        _write_msg(broker_dirs["channels"], "dev", "first msg",
                   timestamp="2026-03-01T00:00:00+00:00")
        _write_msg(broker_dirs["channels"], "dev", "second msg",
                   timestamp="2026-03-02T00:00:00+00:00")
        results = search(query="msg")
        assert len(results) == 2
        assert results[0]["message"] == "second msg"  # Newest first


# ── Channel Filter ───────────────────────────────────────────────────────


class TestSearchByChannel:
    def test_filter_specific_channel(self, broker_dirs):
        from brynq.search import search
        _write_msg(broker_dirs["channels"], "dev", "in dev")
        _write_msg(broker_dirs["channels"], "ops", "in ops")
        results = search(channel="dev")
        assert len(results) == 1
        assert results[0]["message"] == "in dev"

    def test_search_all_channels(self, broker_dirs):
        from brynq.search import search
        _write_msg(broker_dirs["channels"], "dev", "msg-dev")
        _write_msg(broker_dirs["channels"], "ops", "msg-ops")
        results = search()  # No channel filter
        assert len(results) == 2

    def test_nonexistent_channel(self, broker_dirs):
        from brynq.search import search
        results = search(channel="ghost-channel")
        assert len(results) == 0


# ── Sender Filter ────────────────────────────────────────────────────────


class TestSearchBySender:
    def test_filter_by_sender(self, broker_dirs):
        from brynq.search import search
        _write_msg(broker_dirs["channels"], "dev", "from sigma",
                   session_name="Sigma-Engineer")
        _write_msg(broker_dirs["channels"], "dev", "from alpha",
                   session_name="Alpha-Analyst")
        results = search(sender="sigma")
        assert len(results) == 1
        assert results[0]["session_name"] == "Sigma-Engineer"

    def test_sender_substring_match(self, broker_dirs):
        from brynq.search import search
        _write_msg(broker_dirs["channels"], "dev", "hello",
                   session_name="My-Long-Agent-Name")
        results = search(sender="long-agent")
        assert len(results) == 1


# ── Platform Filter ──────────────────────────────────────────────────────


class TestSearchByPlatform:
    def test_filter_by_platform(self, broker_dirs):
        from brynq.search import search
        _write_msg(broker_dirs["channels"], "dev", "from claude",
                   platform="claude")
        _write_msg(broker_dirs["channels"], "dev", "from gemini",
                   platform="gemini")
        results = search(platform="claude")
        assert len(results) == 1
        assert results[0]["platform"] == "claude"

    def test_platform_case_insensitive(self, broker_dirs):
        from brynq.search import search
        _write_msg(broker_dirs["channels"], "dev", "hello",
                   platform="Claude")
        results = search(platform="claude")
        assert len(results) == 1


# ── Message Type Filter ──────────────────────────────────────────────────


class TestSearchByMsgType:
    def test_filter_by_msg_type(self, broker_dirs):
        from brynq.search import search
        _write_msg(broker_dirs["channels"], "dev", "normal msg",
                   msg_type="info")
        _write_msg(broker_dirs["channels"], "dev", "urgent alert",
                   msg_type="alert")
        results = search(msg_type="alert")
        assert len(results) == 1
        assert results[0]["message"] == "urgent alert"


# ── Date Range Filter ────────────────────────────────────────────────────


class TestSearchByDateRange:
    def test_since_filter(self, broker_dirs):
        from brynq.search import search
        _write_msg(broker_dirs["channels"], "dev", "old msg",
                   timestamp="2026-01-01T00:00:00+00:00")
        _write_msg(broker_dirs["channels"], "dev", "new msg",
                   timestamp="2026-03-15T00:00:00+00:00")
        results = search(since="2026-03-01")
        assert len(results) == 1
        assert results[0]["message"] == "new msg"

    def test_until_filter(self, broker_dirs):
        from brynq.search import search
        _write_msg(broker_dirs["channels"], "dev", "old msg",
                   timestamp="2026-01-01T00:00:00+00:00")
        _write_msg(broker_dirs["channels"], "dev", "new msg",
                   timestamp="2026-03-15T00:00:00+00:00")
        results = search(until="2026-02-01")
        assert len(results) == 1
        assert results[0]["message"] == "old msg"

    def test_since_and_until_combined(self, broker_dirs):
        from brynq.search import search
        _write_msg(broker_dirs["channels"], "dev", "too old",
                   timestamp="2026-01-01T00:00:00+00:00")
        _write_msg(broker_dirs["channels"], "dev", "just right",
                   timestamp="2026-02-15T00:00:00+00:00")
        _write_msg(broker_dirs["channels"], "dev", "too new",
                   timestamp="2026-04-01T00:00:00+00:00")
        results = search(since="2026-02-01", until="2026-03-01")
        assert len(results) == 1
        assert results[0]["message"] == "just right"


# ── Limit ────────────────────────────────────────────────────────────────


class TestSearchLimit:
    def test_respects_limit(self, broker_dirs):
        from brynq.search import search
        for i in range(10):
            _write_msg(broker_dirs["channels"], "dev", f"msg-{i}")
        results = search(limit=3)
        assert len(results) == 3

    def test_default_limit_is_20(self, broker_dirs):
        from brynq.search import search
        for i in range(25):
            _write_msg(broker_dirs["channels"], "dev", f"msg-{i}")
        results = search()
        assert len(results) == 20


# ── Combined Filters ─────────────────────────────────────────────────────


class TestSearchCombinedFilters:
    def test_query_plus_platform(self, broker_dirs):
        from brynq.search import search
        _write_msg(broker_dirs["channels"], "dev", "backfill done",
                   platform="claude")
        _write_msg(broker_dirs["channels"], "dev", "backfill done",
                   platform="gemini")
        _write_msg(broker_dirs["channels"], "dev", "tests pass",
                   platform="claude")
        results = search(query="backfill", platform="gemini")
        assert len(results) == 1
        assert results[0]["platform"] == "gemini"

    def test_channel_plus_sender_plus_query(self, broker_dirs):
        from brynq.search import search
        _write_msg(broker_dirs["channels"], "dev", "fix applied",
                   session_name="sigma")
        _write_msg(broker_dirs["channels"], "dev", "fix applied",
                   session_name="alpha")
        _write_msg(broker_dirs["channels"], "ops", "fix applied",
                   session_name="sigma")
        results = search(query="fix", channel="dev", sender="sigma")
        assert len(results) == 1


# ── Format Results ───────────────────────────────────────────────────────


class TestFormatResults:
    def test_format_empty_results(self):
        from brynq.search import format_results
        output = format_results([])
        assert "No messages matched" in output

    def test_format_with_results(self):
        from brynq.search import format_results
        results = [
            {
                "timestamp": "2026-03-15T10:30:00+00:00",
                "session_name": "sigma",
                "platform": "claude",
                "channel": "dev",
                "msg_type": "info",
                "message": "all tests pass",
            }
        ]
        output = format_results(results)
        assert "sigma@claude" in output
        assert "#dev" in output
        assert "all tests pass" in output
        assert "1 match" in output

    def test_format_truncates_timestamp(self):
        from brynq.search import format_results
        results = [{
            "timestamp": "2026-03-15T10:30:00.123456+00:00",
            "session_name": "bot",
            "platform": "cli",
            "channel": "test",
            "msg_type": "info",
            "message": "hello",
        }]
        output = format_results(results)
        # Timestamp should be truncated to 19 chars
        assert "2026-03-15T10:30:00" in output


# ── Internal Helpers ─────────────────────────────────────────────────────


class TestParseDatetime:
    def test_parses_iso8601(self):
        from brynq.search import _parse_datetime
        dt = _parse_datetime("2026-03-15T10:30:00+00:00")
        assert dt is not None
        assert dt.year == 2026
        assert dt.month == 3

    def test_parses_date_only(self):
        from brynq.search import _parse_datetime
        dt = _parse_datetime("2026-03-15")
        assert dt is not None
        assert dt.day == 15

    def test_returns_none_for_garbage(self):
        from brynq.search import _parse_datetime
        assert _parse_datetime("not-a-date") is None
        assert _parse_datetime("") is None
        assert _parse_datetime(None) is None

    def test_naive_datetime_gets_utc(self):
        from brynq.search import _parse_datetime
        from datetime import timezone as tz
        dt = _parse_datetime("2026-03-15T10:00:00")
        assert dt.tzinfo == tz.utc


class TestReadAllEntries:
    def test_reads_valid_jsonl(self, broker_dirs):
        from brynq.search import _read_all_entries
        path = broker_dirs["channels"] / "test.jsonl"
        path.write_text(
            '{"message": "a"}\n{"message": "b"}\n', encoding="utf-8"
        )
        entries = _read_all_entries(path)
        assert len(entries) == 2

    def test_skips_malformed_lines(self, broker_dirs):
        from brynq.search import _read_all_entries
        path = broker_dirs["channels"] / "test.jsonl"
        path.write_text(
            '{"valid": true}\nNOT JSON\n{"also": "valid"}\n',
            encoding="utf-8",
        )
        entries = _read_all_entries(path)
        assert len(entries) == 2

    def test_returns_empty_for_missing_file(self, broker_dirs):
        from brynq.search import _read_all_entries
        path = broker_dirs["channels"] / "nonexistent.jsonl"
        assert _read_all_entries(path) == []

    def test_returns_empty_for_empty_file(self, broker_dirs):
        from brynq.search import _read_all_entries
        path = broker_dirs["channels"] / "empty.jsonl"
        path.write_text("", encoding="utf-8")
        assert _read_all_entries(path) == []
