"""Tests for the executor session adapter: open_chat_session,
build_phase_cmd, scan_codex_session_line, scan_opencode_session_line.

These cover the cursor / codex / claude / opencode wiring inside
``process.py`` — the surface that lets phased builds switch between
executors and resume chat sessions across calls.
"""

import io

import process


# ═══════════════════════════════════════════════════════════════════════════
# build_phase_cmd
# ═══════════════════════════════════════════════════════════════════════════


class TestBuildPhaseCmd:
    def test_cursor_first_call_no_chat_id(self):
        cmd = process.build_phase_cmd(
            "cursor", chat_id=None, model=None, prompt="hi",
        )
        assert cmd[0:2] == [process.CURSOR_CMD, "agent"]
        assert "--print" in cmd
        assert "--yolo" in cmd
        assert "--resume" not in cmd
        assert cmd[-1] == "hi"

    def test_cursor_resume(self):
        cmd = process.build_phase_cmd(
            "cursor", chat_id="abc", model="claude-opus-4-7-thinking-high",
            prompt="hi",
        )
        assert "--resume" in cmd
        # --resume <id> must come as adjacent args
        idx = cmd.index("--resume")
        assert cmd[idx + 1] == "abc"
        idx = cmd.index("--model")
        assert cmd[idx + 1] == "claude-opus-4-7-thinking-high"

    def test_codex_first_call_starts_fresh(self):
        cmd = process.build_phase_cmd(
            "codex", chat_id=None, model=None, prompt="hi",
        )
        assert cmd[0:2] == [process.CODEX_CMD, "exec"]
        assert "--dangerously-bypass-approvals-and-sandbox" in cmd
        assert "resume" not in cmd[:3]

    def test_codex_resume_uses_subcommand(self):
        cmd = process.build_phase_cmd(
            "codex", chat_id="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
            model=None, prompt="hi",
        )
        # Codex uses ``codex exec resume <id>``
        assert cmd[0:3] == [process.CODEX_CMD, "exec", "resume"]
        assert "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" in cmd

    def test_claude_new_session_uses_session_id(self):
        cmd = process.build_phase_cmd(
            "claude", chat_id="aaaa-bbbb", model=None, prompt="hi",
            is_new_session=True,
        )
        assert cmd[0] == process.CLAUDE_CMD
        assert "--session-id" in cmd
        assert "--resume" not in cmd

    def test_claude_resume_uses_resume(self):
        cmd = process.build_phase_cmd(
            "claude", chat_id="aaaa-bbbb", model="sonnet", prompt="hi",
            is_new_session=False,
        )
        assert "--resume" in cmd
        assert "--session-id" not in cmd
        idx = cmd.index("--model")
        assert cmd[idx + 1] == "sonnet"

    # -- opencode --------------------------------------------------------

    def test_opencode_first_call(self):
        cmd = process.build_phase_cmd(
            "opencode", chat_id=None, model=None, prompt="do work",
        )
        assert cmd[0:2] == [process.OPENCODE_CMD, "run"]
        # JSON streaming is required so we can scrape the session id.
        idx = cmd.index("--format")
        assert cmd[idx + 1] == "json"
        assert "--dangerously-skip-permissions" in cmd
        assert "--session" not in cmd
        assert cmd[-1] == "do work"

    def test_opencode_resume(self):
        cmd = process.build_phase_cmd(
            "opencode", chat_id="ses_abc123", model=None, prompt="next",
        )
        assert "--session" in cmd
        idx = cmd.index("--session")
        assert cmd[idx + 1] == "ses_abc123"
        # Still keep the JSON stream so we never lose the session id.
        assert "json" in cmd

    def test_opencode_with_model(self):
        cmd = process.build_phase_cmd(
            "opencode", chat_id=None,
            model="anthropic/claude-sonnet-4-5", prompt="hi",
        )
        idx = cmd.index("--model")
        assert cmd[idx + 1] == "anthropic/claude-sonnet-4-5"


# ═══════════════════════════════════════════════════════════════════════════
# open_chat_session — branches that don't shell out
# ═══════════════════════════════════════════════════════════════════════════


class TestOpenChatSession:
    def test_codex_returns_none(self):
        # Codex creates the session implicitly on the first exec call.
        assert process.open_chat_session(
            "codex", cwd=".", log_fh=io.StringIO(),
        ) is None

    def test_opencode_returns_none(self):
        # OpenCode creates the session implicitly on the first run call.
        assert process.open_chat_session(
            "opencode", cwd=".", log_fh=io.StringIO(),
        ) is None

    def test_claude_pregenerates_uuid(self):
        sid = process.open_chat_session(
            "claude", cwd=".", log_fh=io.StringIO(),
        )
        assert sid is not None
        # Standard UUID v4 length / shape (36 chars including dashes).
        assert len(sid) == 36
        assert sid.count("-") == 4

    def test_unknown_executor_returns_none_and_logs(self):
        log = io.StringIO()
        sid = process.open_chat_session("foobar", cwd=".", log_fh=log)
        assert sid is None
        assert "unknown executor" in log.getvalue()


# ═══════════════════════════════════════════════════════════════════════════
# Session-id scrapers
# ═══════════════════════════════════════════════════════════════════════════


class TestScanCodexSessionLine:
    def test_extracts_uuid_from_banner(self):
        line = "session id: 12345678-1234-1234-1234-123456789abc"
        assert process.scan_codex_session_line(line) == \
            "12345678-1234-1234-1234-123456789abc"

    def test_ignores_lines_without_session_hint(self):
        # No "session" / "conversation" hint → no match even with UUID.
        line = "Working on task 12345678-1234-1234-1234-123456789abc"
        assert process.scan_codex_session_line(line) is None

    def test_ignores_lines_with_no_uuid(self):
        assert process.scan_codex_session_line("session id: missing") is None


class TestScanOpencodeSessionLine:
    def test_extracts_session_id_from_json_event(self):
        # Real-shape opencode event line.
        line = (
            '{"type":"step_start","timestamp":1759406013703,'
            '"sessionID":"ses_65b3acf58ffeLSa4dfj1RVoPpW","part":{}}'
        )
        assert process.scan_opencode_session_line(line) == \
            "ses_65b3acf58ffeLSa4dfj1RVoPpW"

    def test_ignores_uuid_format(self):
        # Codex-style UUID must NOT match — opencode IDs use ses_ prefix.
        line = '{"sessionID":"12345678-1234-1234-1234-123456789abc"}'
        assert process.scan_opencode_session_line(line) is None

    def test_ignores_non_session_lines(self):
        assert process.scan_opencode_session_line("Loading models…") is None
        assert process.scan_opencode_session_line("") is None

    def test_handles_whitespace_in_json(self):
        # Pretty-printed events still match.
        line = '  "sessionID" : "ses_abc123XYZ"  ,'
        assert process.scan_opencode_session_line(line) == "ses_abc123XYZ"
