"""Tests for Telegram session persistence."""

import json
import time
from datetime import datetime, timezone, timedelta
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from fliiq.runtime.telegram.session_store import TelegramSessionStore


@pytest.fixture
def store(tmp_path):
    return TelegramSessionStore(tmp_path)


# ── Basic write/read ─────────────────────────────────────────────────


def test_log_message_creates_file(store, tmp_path):
    store.log_message(123, "user", "hello")
    path = tmp_path / "telegram_sessions" / "123.jsonl"
    assert path.exists()
    lines = path.read_text().strip().split("\n")
    assert len(lines) == 1
    entry = json.loads(lines[0])
    assert entry["role"] == "user"
    assert entry["text"] == "hello"
    assert "ts" in entry


def test_log_message_appends(store, tmp_path):
    store.log_message(123, "user", "msg1")
    store.log_message(123, "assistant", "msg2")
    path = tmp_path / "telegram_sessions" / "123.jsonl"
    lines = path.read_text().strip().split("\n")
    assert len(lines) == 2
    assert json.loads(lines[0])["role"] == "user"
    assert json.loads(lines[1])["role"] == "assistant"


def test_load_recent_empty(store):
    messages = store.load_recent(999)
    assert messages == []


def test_load_recent_roundtrip(store):
    store.log_message(42, "user", "hi")
    store.log_message(42, "assistant", "hello back")
    messages = store.load_recent(42)
    assert len(messages) == 2
    assert messages[0]["role"] == "user"
    assert "hi" in messages[0]["content"]
    assert '<external_message' in messages[0]["content"]
    assert messages[1]["role"] == "assistant"
    assert messages[1]["content"] == "hello back"


# ── Thread ID support ────────────────────────────────────────────────


def test_thread_id_separate_files(store, tmp_path):
    store.log_message(42, "user", "no thread")
    store.log_message(42, "user", "thread 101", thread_id=101)
    store.log_message(42, "user", "thread 202", thread_id=202)

    assert (tmp_path / "telegram_sessions" / "42.jsonl").exists()
    assert (tmp_path / "telegram_sessions" / "42_thread_101.jsonl").exists()
    assert (tmp_path / "telegram_sessions" / "42_thread_202.jsonl").exists()


def test_load_recent_with_thread_id(store):
    store.log_message(42, "user", "thread msg", thread_id=101)
    store.log_message(42, "assistant", "thread reply", thread_id=101)

    # Load for thread 101
    msgs = store.load_recent(42, thread_id=101)
    assert len(msgs) == 2
    assert 'message_thread_id="101"' in msgs[0]["content"]

    # Load for no thread — should be empty
    msgs_main = store.load_recent(42)
    assert msgs_main == []


# ── Max messages limit ───────────────────────────────────────────────


def test_max_messages_limit(store):
    for i in range(30):
        store.log_message(42, "user", f"msg {i}")
    messages = store.load_recent(42, max_messages=10)
    assert len(messages) == 10
    # Should be the LAST 10
    assert "msg 20" in messages[0]["content"]
    assert "msg 29" in messages[9]["content"]


# ── Age filter ───────────────────────────────────────────────────────


def test_age_filter(store, tmp_path):
    """Messages older than max_age_hours are excluded."""
    path = tmp_path / "telegram_sessions" / "42.jsonl"
    path.parent.mkdir(parents=True, exist_ok=True)

    # Write an old message (48h ago)
    old_ts = (datetime.now(timezone.utc) - timedelta(hours=48)).isoformat()
    old_entry = json.dumps({"role": "user", "text": "old msg", "ts": old_ts})

    # Write a recent message
    recent_ts = datetime.now(timezone.utc).isoformat()
    recent_entry = json.dumps({"role": "user", "text": "new msg", "ts": recent_ts})

    path.write_text(f"{old_entry}\n{recent_entry}\n")

    messages = store.load_recent(42, max_age_hours=24)
    assert len(messages) == 1
    assert "new msg" in messages[0]["content"]


# ── Malformed line handling ──────────────────────────────────────────


def test_malformed_lines_skipped(store, tmp_path):
    path = tmp_path / "telegram_sessions" / "42.jsonl"
    path.parent.mkdir(parents=True, exist_ok=True)
    ts = datetime.now(timezone.utc).isoformat()
    path.write_text(
        f'not json\n'
        f'{{"role": "user", "text": "good", "ts": "{ts}"}}\n'
        f'{{"role": "user", "missing_ts": true}}\n'
    )
    messages = store.load_recent(42)
    assert len(messages) == 1
    assert "good" in messages[0]["content"]


# ── External message tagging ─────────────────────────────────────────


def test_user_messages_tagged(store):
    store.log_message(42, "user", "hi there")
    messages = store.load_recent(42)
    content = messages[0]["content"]
    assert content.startswith('<external_message source="telegram" chat_id="42">')
    assert "hi there" in content
    assert content.endswith("</external_message>")


def test_assistant_messages_plain(store):
    store.log_message(42, "assistant", "bot reply")
    messages = store.load_recent(42)
    assert messages[0]["content"] == "bot reply"
    assert "<external_message" not in messages[0]["content"]


# ── Integration: send_telegram logs outbound ─────────────────────────


async def test_send_telegram_logs_outbound(tmp_path, monkeypatch):
    """send_telegram handler logs outbound message to session store."""
    monkeypatch.setenv("TELEGRAM_BOT_TOKEN", "fake-token")
    monkeypatch.setattr(
        "fliiq.runtime.config.global_fliiq_dir",
        lambda: tmp_path,
    )

    mock_response = MagicMock()
    mock_response.status_code = 200
    mock_response.json.return_value = {"result": {"message_id": 99}}

    with patch("httpx.AsyncClient") as mock_client_cls:
        mock_client = AsyncMock()
        mock_client.post.return_value = mock_response
        mock_client_cls.return_value.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client_cls.return_value.__aexit__ = AsyncMock(return_value=False)

        from fliiq.data.skills.core.send_telegram.main import handler
        result = await handler({"chat_id": 42, "text": "Job reminder"})

    assert result["message_id"] == 99

    # Verify outbound was logged
    store = TelegramSessionStore(tmp_path)
    messages = store.load_recent(42)
    assert len(messages) == 1
    assert messages[0]["role"] == "assistant"
    assert messages[0]["content"] == "Job reminder"


async def test_send_telegram_logs_with_thread_id(tmp_path, monkeypatch):
    """send_telegram logs thread_id when provided."""
    monkeypatch.setenv("TELEGRAM_BOT_TOKEN", "fake-token")
    monkeypatch.setattr(
        "fliiq.runtime.config.global_fliiq_dir",
        lambda: tmp_path,
    )

    mock_response = MagicMock()
    mock_response.status_code = 200
    mock_response.json.return_value = {"result": {"message_id": 100}}

    with patch("httpx.AsyncClient") as mock_client_cls:
        mock_client = AsyncMock()
        mock_client.post.return_value = mock_response
        mock_client_cls.return_value.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client_cls.return_value.__aexit__ = AsyncMock(return_value=False)

        from fliiq.data.skills.core.send_telegram.main import handler
        await handler({"chat_id": 42, "text": "Thread msg", "message_thread_id": 101})

    store = TelegramSessionStore(tmp_path)
    messages = store.load_recent(42, thread_id=101)
    assert len(messages) == 1
    assert messages[0]["content"] == "Thread msg"


# ── Integration: listener seeds from persistent store ─────────────────


async def test_listener_seeds_from_store(tmp_path):
    """New session loads recent messages from persistent store."""
    # Pre-populate the store (simulating a prior job sending a message)
    store = TelegramSessionStore(tmp_path)
    store.log_message(42, "assistant", "Time to call Sarah!")

    listener = _make_listener(tmp_path)

    with patch(
        "fliiq.runtime.telegram.listener.setup_agent_resources",
        new_callable=AsyncMock,
    ) as mock_setup:
        mock_setup.return_value = MagicMock()
        session = await listener._get_or_create_session(42)

    # Session should contain the pre-existing outbound message
    messages = session["messages"]
    assert len(messages) == 1
    assert messages[0]["role"] == "assistant"
    assert messages[0]["content"] == "Time to call Sarah!"


async def test_listener_logs_inbound_and_outbound(tmp_path):
    """_handle_message logs both inbound user message and outbound response."""
    from fliiq.runtime.telegram.listener import TelegramListener

    listener = _make_listener(tmp_path)

    with (
        patch.object(listener, "_get_or_create_session", new_callable=AsyncMock) as mock_session,
        patch("fliiq.runtime.telegram.listener.agent_loop", new_callable=AsyncMock) as mock_loop,
        patch.object(listener, "send_message", new_callable=AsyncMock),
        patch.object(listener, "_send_chat_action", new_callable=AsyncMock),
        patch("fliiq.runtime.telegram.listener.assemble_agent_prompt", return_value="system"),
    ):
        mock_result = MagicMock()
        mock_result.messages = []
        mock_result.final_text = "I can help with that"
        mock_result.iterations = 0
        mock_loop.return_value = mock_result

        mock_resources = MagicMock()
        mock_resources.soul = ""
        mock_resources.user_soul = ""
        mock_resources.skill_info = None
        mock_resources.memory_context = ""
        mock_resources.user_profile = ""
        mock_resources.fliiq_email = ""
        mock_session.return_value = {
            "messages": [],
            "resources": mock_resources,
            "model_override": None,
            "model_override_display": None,
        }

        await listener._handle_message(42, "help me recreate the job")

    # Check persistent store
    store = TelegramSessionStore(tmp_path)
    messages = store.load_recent(42)
    assert len(messages) == 2
    assert messages[0]["role"] == "user"
    assert "help me recreate the job" in messages[0]["content"]
    assert messages[1]["role"] == "assistant"
    assert messages[1]["content"] == "I can help with that"


async def test_listener_no_double_log_when_agent_sends(tmp_path):
    """When agent sends via send_telegram tool, listener doesn't double-log outbound."""
    listener = _make_listener(tmp_path)

    with (
        patch.object(listener, "_get_or_create_session", new_callable=AsyncMock) as mock_session,
        patch("fliiq.runtime.telegram.listener.agent_loop", new_callable=AsyncMock) as mock_loop,
        patch.object(listener, "send_message", new_callable=AsyncMock) as mock_send,
        patch.object(listener, "_send_chat_action", new_callable=AsyncMock),
        patch("fliiq.runtime.telegram.listener.assemble_agent_prompt", return_value="system"),
    ):
        # Agent result includes a send_telegram tool call
        mock_result = MagicMock()
        mock_result.messages = [
            {"role": "assistant", "content": [
                {"type": "tool_use", "name": "send_telegram", "id": "t1",
                 "input": {"chat_id": 42, "text": "Sent via tool"}},
            ]},
        ]
        mock_result.final_text = "Done"
        mock_result.iterations = 1
        mock_loop.return_value = mock_result

        mock_resources = MagicMock()
        mock_resources.soul = ""
        mock_resources.user_soul = ""
        mock_resources.skill_info = None
        mock_resources.memory_context = ""
        mock_resources.user_profile = ""
        mock_resources.fliiq_email = ""
        mock_session.return_value = {
            "messages": [],
            "resources": mock_resources,
            "model_override": None,
            "model_override_display": None,
        }

        await listener._handle_message(42, "test message")

    # send_message NOT called (agent sent via tool)
    mock_send.assert_not_called()

    # Only the inbound message should be logged (tool handler logs the outbound)
    store = TelegramSessionStore(tmp_path)
    messages = store.load_recent(42)
    assert len(messages) == 1
    assert messages[0]["role"] == "user"


# ── Helpers ───────────────────────────────────────────────────────────


def _make_listener(tmp_path):
    from fliiq.runtime.telegram.listener import TelegramListener
    return TelegramListener(
        bot_token="fake-token",
        llm_config=MagicMock(),
        project_root=tmp_path,
        fliiq_dir=tmp_path,
    )
