"""Tests for the CLI surface (spens/cli.py).

Flag-validation matrix for the non-interactive output modes: jsonl /
background require a prompt (yolo mode) with a yolo-capable agent and exactly
one of --accept-changes / --reject-changes, enforced BEFORE any session
directory is created.  Plus --session-id validation and the bare error event.
"""

from __future__ import annotations

import json

import pytest
from spens.cli import _build_run_parser, _emit_bare_error
from spens.runner import (
    SESSION_ID_RE,
    SpensUsageError,
    run_session,
    validate_non_interactive,
    validate_session_id,
)


def _agent(**fields) -> dict:
    return {
        "template_type": "agent",
        "name": "a",
        "installation_command": "true",
        "yolo_command": "a run {prompt}",
        **fields,
    }


NO_YOLO = {"template_type": "agent", "name": "a", "installation_command": "true"}


# -- flag validation matrix ---------------------------------------------------


def test_jsonl_without_prompt_is_rejected() -> None:
    with pytest.raises(SpensUsageError, match="prompt"):
        validate_non_interactive("jsonl", None, _agent(), False, True)


def test_jsonl_without_yolo_command_agent_is_rejected() -> None:
    with pytest.raises(SpensUsageError, match="yolo_command"):
        validate_non_interactive("jsonl", "hi", NO_YOLO, True, False)


def test_both_change_flags_is_an_error() -> None:
    with pytest.raises(SpensUsageError, match="exactly one"):
        validate_non_interactive("jsonl", "hi", _agent(), True, True)


def test_neither_change_flag_is_an_error() -> None:
    with pytest.raises(SpensUsageError, match="exactly one"):
        validate_non_interactive("background", "hi", _agent(), False, False)


def test_valid_non_interactive_configuration_passes() -> None:
    validate_non_interactive("jsonl", "hi", _agent(), True, False)
    validate_non_interactive("background", "hi", _agent(), False, True)


def test_tty_mode_has_no_non_interactive_requirements() -> None:
    # no prompt, no change flags, no yolo_command: all fine in tty mode
    validate_non_interactive("tty", None, NO_YOLO, False, False)


def _write_yolo_agent(ws) -> None:
    (ws / "templates").mkdir(exist_ok=True)
    (ws / "templates" / "yolo.json").write_text(json.dumps(_agent(name="yolo")))


def test_validation_happens_before_any_session_state(tmp_path) -> None:
    """A rejected non-interactive run must not create session directories."""
    ws = tmp_path / "ws"
    ws.mkdir()
    _write_yolo_agent(ws)

    spens_dir = tmp_path / "state"
    with pytest.raises(SpensUsageError):
        run_session(
            "node-20", "yolo", ws,
            prompt=None,                      # missing prompt
            accept_changes=True,
            output_mode="jsonl",
            spens_dir=spens_dir,
        )
    assert not spens_dir.exists()
    assert not (ws / ".spens").exists()


# -- bare error event ---------------------------------------------------------


def test_emit_bare_error_is_one_json_line(capsys) -> None:
    _emit_bare_error("[spens] Error: nope")
    lines = capsys.readouterr().out.splitlines()
    assert len(lines) == 1
    payload = json.loads(lines[0])
    assert payload == {
        "schema_version": 1,
        "event": "error",
        "session_id": "",
        "timestamp": payload["timestamp"],
        "data": {"message": "[spens] Error: nope"},
    }


def test_bare_error_timestamp_is_iso8601_utc(capsys) -> None:
    from datetime import datetime

    _emit_bare_error("x")
    payload = json.loads(capsys.readouterr().out)
    parsed = datetime.fromisoformat(payload["timestamp"])
    assert parsed.utcoffset().total_seconds() == 0


# -- --session-id -------------------------------------------------------------


@pytest.mark.parametrize("bad", [
    "UPPER", "with_underscore", "with space", "-leading-dash",
    "a" * 33, "", "dot.included",
])
def test_invalid_session_ids_rejected(bad: str) -> None:
    with pytest.raises(SpensUsageError):
        validate_session_id(bad)


@pytest.mark.parametrize("good", ["a", "abc123", "a-b-c", "0-start", "x" * 32])
def test_valid_session_ids_accepted(good: str) -> None:
    validate_session_id(good)


def test_session_id_regex_shape() -> None:
    assert SESSION_ID_RE == r"^[a-z0-9][a-z0-9-]{0,31}$"


def test_session_id_collision_is_an_error_not_an_overwrite(tmp_path) -> None:
    ws = tmp_path / "ws"
    ws.mkdir()
    _write_yolo_agent(ws)
    spens_dir = tmp_path / "state"
    existing = spens_dir / "sessions" / "reused-1"
    existing.mkdir(parents=True)
    (existing / "marker.txt").write_text("keep me")

    with pytest.raises(SpensUsageError, match="already exists"):
        run_session(
            "node-20", "yolo", ws,
            prompt="hi", accept_changes=True,
            output_mode="jsonl",
            spens_dir=spens_dir,
            session_id="reused-1",
        )
    # the existing session directory is untouched
    assert (existing / "marker.txt").read_text() == "keep me"


# -- parser surface ------------------------------------------------------------


def test_parser_accepts_new_flags() -> None:
    args = _build_run_parser().parse_args([
        "node-20", "opencode", "ws", "do it",
        "--output", "jsonl", "--reject-changes", "--session-id", "abc-1",
        "--accept-changes",
    ])
    assert args.output == "jsonl"
    assert args.reject_changes is True
    assert args.accept_changes is True
    assert args.session_id == "abc-1"


def test_parser_defaults_match_today() -> None:
    args = _build_run_parser().parse_args(["node-20", "opencode", "ws"])
    assert args.output == "tty"
    assert args.reject_changes is False
    assert args.accept_changes is False
    assert args.session_id is None
    assert args.prompt is None


def test_parser_rejects_unknown_output_mode() -> None:
    with pytest.raises(SystemExit):
        _build_run_parser().parse_args(["e", "a", "w", "--output", "nope"])


def test_internal_reuse_flag_is_hidden_but_parsed() -> None:
    args = _build_run_parser().parse_args(["e", "a", "w", "p", "--_reuse-session-id", "sid"])
    assert args._reuse_session_id == "sid"
