"""Tests for maniple.idle_detection module."""

from __future__ import annotations

import os
import time
from pathlib import Path

import pytest

from maniple.idle_detection import (
    Worker,
    check_file_idle,
    detect_worker_idle,
    get_claude_jsonl_path,
    get_project_slug,
)


class TestProjectSlug:
    """Tests for project slug helpers."""

    def test_get_project_slug_replaces_slashes_and_dots(self):
        """get_project_slug should replace '/' and '.' with '-' characters."""
        assert get_project_slug("/Users/josh/code") == "-Users-josh-code"
        assert get_project_slug("/path/.worktrees/foo") == "-path--worktrees-foo"


class TestClaudeJsonlPath:
    """Tests for Claude JSONL path construction."""

    def test_get_claude_jsonl_path_builds_expected_path(self, tmp_path, monkeypatch):
        """Should build JSONL path using Claude project slug format."""
        monkeypatch.setattr(Path, "home", lambda: tmp_path)
        worker = Worker(project_path="/Users/josh/code", claude_session_id="abc123", agent_type="claude")
        expected = tmp_path / ".claude" / "projects" / "-Users-josh-code" / "abc123.jsonl"
        assert get_claude_jsonl_path(worker) == expected

    def test_get_claude_jsonl_path_missing_fields(self):
        """Should return None when project_path or session_id missing."""
        worker = Worker(project_path="", claude_session_id=None, agent_type="claude")
        assert get_claude_jsonl_path(worker) is None


class TestCheckFileIdle:
    """Tests for file mtime checks."""

    def test_check_file_idle_true_when_old(self, tmp_path):
        """Should report idle when file age exceeds threshold."""
        path = tmp_path / "sample.jsonl"
        path.write_text("data")
        os.utime(path, (time.time() - 10, time.time() - 10))

        is_idle, age = check_file_idle(path, threshold_seconds=5)
        assert is_idle is True
        assert age >= 5

    def test_check_file_idle_false_when_recent(self, tmp_path):
        """Should report not idle when file is recent."""
        path = tmp_path / "sample.jsonl"
        path.write_text("data")
        os.utime(path, (time.time(), time.time()))

        is_idle, age = check_file_idle(path, threshold_seconds=5)
        assert is_idle is False
        assert age >= 0


class TestDetectWorkerIdleClaude:
    """Tests for detect_worker_idle with Claude workers."""

    def test_claude_idle_from_jsonl_mtime(self, tmp_path, monkeypatch):
        """Should detect idle based on JSONL mtime for Claude workers."""
        monkeypatch.setattr(Path, "home", lambda: tmp_path)
        worker = Worker(project_path="/Users/josh/code", claude_session_id="abc123", agent_type="claude")
        jsonl_path = get_claude_jsonl_path(worker)
        assert jsonl_path is not None
        jsonl_path.parent.mkdir(parents=True)
        jsonl_path.write_text("data")

        os.utime(jsonl_path, (time.time() - 10, time.time() - 10))
        is_idle, reason = detect_worker_idle(worker, idle_threshold_seconds=5)
        assert is_idle is True
        assert reason and reason.startswith("jsonl_mtime:")

    def test_claude_not_idle_when_jsonl_recent(self, tmp_path, monkeypatch):
        """Should report active when JSONL mtime is recent."""
        monkeypatch.setattr(Path, "home", lambda: tmp_path)
        worker = Worker(project_path="/Users/josh/code", claude_session_id="abc123", agent_type="claude")
        jsonl_path = get_claude_jsonl_path(worker)
        assert jsonl_path is not None
        jsonl_path.parent.mkdir(parents=True)
        jsonl_path.write_text("data")

        os.utime(jsonl_path, (time.time(), time.time()))
        is_idle, reason = detect_worker_idle(worker, idle_threshold_seconds=5)
        assert is_idle is False
        assert reason is None

    def test_claude_idle_from_message_count(self):
        """Should use message_count when JSONL missing."""
        now = time.time()
        worker = Worker(
            project_path="/Users/josh/code",
            claude_session_id="abc123",
            agent_type="claude",
            message_count=10,
            last_message_count=10,
            last_message_timestamp=now - 400,
        )

        is_idle, reason = detect_worker_idle(worker, idle_threshold_seconds=300)
        assert is_idle is True
        assert reason and reason.startswith("message_count_stalled:")

    def test_claude_preserves_state_when_no_signal(self):
        """Should return existing state when no idle signal is available."""
        worker = Worker(project_path="/Users/josh/code", claude_session_id="abc123", agent_type="claude")
        worker.is_idle = True

        is_idle, reason = detect_worker_idle(worker, idle_threshold_seconds=300)
        assert is_idle is True
        assert reason is None


class TestDetectWorkerIdleCodex:
    """Tests for detect_worker_idle with Codex workers."""

    def test_codex_idle_from_output_mtime(self, tmp_path):
        """Should detect idle based on output file mtime for Codex workers."""
        output_path = tmp_path / "output.txt"
        output_path.write_text("done")
        os.utime(output_path, (time.time() - 10, time.time() - 10))

        worker = Worker(project_path="/Users/josh/code", claude_session_id=None, agent_type="codex")
        worker.output_path = output_path

        is_idle, reason = detect_worker_idle(worker, idle_threshold_seconds=5)
        assert is_idle is True
        assert reason and reason.startswith("output_mtime:")

    def test_codex_idle_when_process_exited(self, monkeypatch):
        """Should detect idle when Codex process no longer exists."""
        worker = Worker(project_path="/Users/josh/code", claude_session_id=None, agent_type="codex", pid=1234)

        def _raise(*_args, **_kwargs):
            raise OSError("no such process")

        monkeypatch.setattr(os, "kill", _raise)

        is_idle, reason = detect_worker_idle(worker, idle_threshold_seconds=300)
        assert is_idle is True
        assert reason == "process_exited"

    def test_codex_idle_when_process_sleeping(self, monkeypatch):
        """Should detect idle when Codex process is sleeping."""
        worker = Worker(project_path="/Users/josh/code", claude_session_id=None, agent_type="codex", pid=4321)

        monkeypatch.setattr(os, "kill", lambda *_args, **_kwargs: None)

        class Result:
            stdout = "S"

        monkeypatch.setattr(
            "maniple.idle_detection.subprocess.run",
            lambda *_args, **_kwargs: Result(),
        )

        is_idle, reason = detect_worker_idle(worker, idle_threshold_seconds=300)
        assert is_idle is True
        assert reason == "process_sleeping"

    def test_codex_preserves_state_when_no_signal(self):
        """Should return existing state when no Codex signal is available."""
        worker = Worker(project_path="/Users/josh/code", claude_session_id=None, agent_type="codex")
        worker.is_idle = False

        is_idle, reason = detect_worker_idle(worker, idle_threshold_seconds=300)
        assert is_idle is False
        assert reason is None
