"""Validate cron/jobs.json schema per kind.

Every job must be either `kind: "shell"` (with command/report_on/channel)
or Claude-style (prompt/model/channel). Required fields are enforced
per kind. Cron expressions must parse.
"""

from __future__ import annotations

import json
from pathlib import Path

import pytest
from apscheduler.triggers.cron import CronTrigger

PROJECT_ROOT = Path(__file__).resolve().parents[1]
JOBS_FILE = PROJECT_ROOT / "cron" / "jobs.json"

VALID_REPORT_ON = {"always", "changes", "never"}
# Keep in sync with src.claude_session.VALID_MODELS (informational — we don't
# import there to avoid pulling the whole module into the schema test).
VALID_MODELS = {"haiku", "sonnet", "opus"}


@pytest.fixture(scope="module")
def jobs():
    text = JOBS_FILE.read_text(encoding="utf-8")
    data = json.loads(text)
    assert isinstance(data, list), "jobs.json must be a JSON array"
    return data


def test_jobs_file_exists_and_parses(jobs):
    assert isinstance(jobs, list)


def test_names_are_unique(jobs):
    names = [j["name"] for j in jobs]
    assert len(names) == len(set(names)), f"duplicate names: {names}"


def test_every_job_has_name_and_cron(jobs):
    for j in jobs:
        assert "name" in j and isinstance(j["name"], str) and j["name"]
        assert "cron" in j and isinstance(j["cron"], str) and j["cron"]


def test_every_cron_expression_parses(jobs):
    for j in jobs:
        try:
            CronTrigger.from_crontab(j["cron"])
        except Exception as exc:  # pragma: no cover — diagnostic
            pytest.fail(f"job {j['name']!r} has invalid cron {j['cron']!r}: {exc}")


def test_enabled_is_bool(jobs):
    for j in jobs:
        assert isinstance(j.get("enabled"), bool), (
            f"job {j['name']!r} missing/non-bool enabled"
        )


def test_shell_jobs_have_required_fields(jobs):
    shell_jobs = [j for j in jobs if j.get("kind") == "shell"]
    assert shell_jobs, "expected at least one shell job"

    for j in shell_jobs:
        name = j["name"]
        # channel
        assert isinstance(j.get("channel"), str) and j["channel"], (
            f"shell job {name!r} has empty/missing channel"
        )
        # command
        cmd = j.get("command")
        assert isinstance(cmd, list) and cmd, (
            f"shell job {name!r} has empty/missing command"
        )
        assert all(isinstance(c, str) and c.strip() for c in cmd), (
            f"shell job {name!r} has non-string entry in command: {cmd!r}"
        )
        # report_on
        report_on = j.get("report_on")
        assert report_on in VALID_REPORT_ON, (
            f"shell job {name!r} has invalid report_on={report_on!r}"
        )
        # timeout (optional but if present must be a positive int)
        to = j.get("timeout")
        if to is not None:
            assert isinstance(to, int) and not isinstance(to, bool), (
                f"shell job {name!r} has non-int timeout"
            )
            assert 1 <= to <= 3600, (
                f"shell job {name!r} timeout out of range: {to}"
            )


def test_claude_jobs_have_required_fields(jobs):
    claude_jobs = [j for j in jobs if j.get("kind", "claude") == "claude"]
    assert claude_jobs, "expected at least one claude job"

    for j in claude_jobs:
        name = j["name"]
        # channel
        assert isinstance(j.get("channel"), str) and j["channel"], (
            f"claude job {name!r} has empty/missing channel"
        )
        # prompt
        prompt = j.get("prompt")
        assert isinstance(prompt, str) and prompt.strip(), (
            f"claude job {name!r} has empty/missing prompt"
        )
        # model
        model = j.get("model")
        assert model in VALID_MODELS, (
            f"claude job {name!r} has invalid model={model!r}"
        )
        # allowed_tools
        allowed = j.get("allowed_tools")
        assert isinstance(allowed, list), (
            f"claude job {name!r} has non-list allowed_tools"
        )
        for t in allowed:
            assert isinstance(t, str), (
                f"claude job {name!r} has non-string tool: {t!r}"
            )


def test_shell_jobs_reference_existing_scripts(jobs):
    """Every shell command's first path-looking argument must refer to a file
    that exists in the repo (so we don't ship a jobs.json that runs missing
    scripts on day one)."""
    shell_jobs = [j for j in jobs if j.get("kind") == "shell"]
    for j in shell_jobs:
        cmd = j["command"]
        # Find the first argument that looks like a relative path
        # (contains "/" and doesn't start with "-").
        script = next(
            (a for a in cmd[1:] if "/" in a and not a.startswith("-")),
            None,
        )
        if script is None:
            continue
        script_path = PROJECT_ROOT / script
        assert script_path.exists(), (
            f"shell job {j['name']!r} references missing script: {script}"
        )


def test_no_clawd_paths_remain_in_claude_prompts(jobs):
    """Sanity check: no imported prompt still points at /home/moltbot/clawd/."""
    offenders: list[str] = []
    for j in jobs:
        if j.get("kind", "claude") != "claude":
            continue
        prompt = j.get("prompt", "")
        if "/home/moltbot/clawd/" in prompt:
            offenders.append(j["name"])
        if "cd ~/clawd " in prompt or prompt.endswith("cd ~/clawd") or "cd ~/clawd\n" in prompt:
            offenders.append(j["name"])
    assert not offenders, (
        f"these claude jobs still reference /home/moltbot/clawd/ or cd ~/clawd: "
        f"{offenders}"
    )
