//! Guards that the published JSON schema stays a clean public contract.
//!
//! The schema at `schemas/v1.json` ships to users and is served at
//! `https://mcptest.sh/schema/v1.json`. These tests exist because internal
//! tracker identifiers and unmarked preview constructs leaked into that public
//! surface once before, so we pin the invariants in CI.

use std::path::PathBuf;

/// Locate the published schema relative to this crate.
fn schema_path() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .and_then(|p| p.parent())
        .map(|root| root.join("schemas").join("v1.json"))
        .expect("workspace root has a schemas/ directory")
}

/// Read the published schema text from disk.
fn schema_text() -> String {
    let path = schema_path();
    std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()))
}

/// The public schema must never expose an internal Linear ticket id like
/// `WOR-281`. Authors and IDE users read these strings; tracker ids are noise
/// at best and an information leak at worst.
#[test]
fn published_schema_has_no_internal_ticket_ids() {
    let text = schema_text();
    // Match the literal prefix plus a digit; this is the shape of every
    // internal id (`WOR-<number>`) without pulling in a regex dependency.
    let mut offenders = Vec::new();
    for (lineno, line) in text.lines().enumerate() {
        if let Some(pos) = line.find("WOR-") {
            let tail = &line[pos + 4..];
            if tail
                .chars()
                .next()
                .map(|c| c.is_ascii_digit())
                .unwrap_or(false)
            {
                offenders.push(format!("line {}: {}", lineno + 1, line.trim()));
            }
        }
    }
    assert!(
        offenders.is_empty(),
        "published schema leaks internal ticket ids:\n{}",
        offenders.join("\n")
    );
}

/// The schema must stay valid JSON. A broken contract breaks every consumer.
#[test]
fn published_schema_is_valid_json() {
    let text = schema_text();
    let parsed: serde_json::Value =
        serde_json::from_str(&text).expect("schemas/v1.json must be valid JSON");
    assert!(parsed.is_object(), "schema root must be a JSON object");
}

/// Constructs the runner does not yet honor must carry a machine-readable
/// `x-mcptest-status: preview` marker, not just prose. This lets tooling and
/// docs distinguish committed-and-enforced fields from preview ones. The marker
/// rides alongside several deferred constructs (the multi-server forms, the
/// setup/teardown lifecycle blocks, and the stepwise tests), so its presence is
/// a cheap signal that the convention is intact. (`llm-judge` carried the marker
/// in v1.0 but is now enforced, so it no longer does, WOR-1425.)
#[test]
fn deferred_constructs_carry_preview_status() {
    let text = schema_text();
    assert!(
        text.contains("\"x-mcptest-status\": \"preview\""),
        "expected at least one x-mcptest-status: preview marker on a deferred construct"
    );
}
