//! Lightweight checks that docs/ files cover the current public
//! contracts. Greps for stable keywords so that any rename or
//! omission breaks the test loudly. Not a substitute for hand-reading
//! the docs.

fn read(rel: &str) -> String {
    // tests run from the crate directory.
    let path = format!("../../{rel}");
    std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("could not read {path}: {e}"))
}

const PUBLIC_DOCS: &[&str] = &[
    "README.md",
    "docs/getting-started.md",
    "docs/resources.md",
    "docs/events.md",
    "docs/event-envelope.md",
    "docs/caql.md",
    "docs/peers.md",
    "examples/hello-led/README.md",
    "examples/job-runner/README.md",
    "crates/boardwalk/src/lib.rs",
];

const PUBLIC_MARKDOWN_DOCS: &[&str] = &[
    "README.md",
    "docs/getting-started.md",
    "docs/resources.md",
    "docs/events.md",
    "docs/event-envelope.md",
    "docs/caql.md",
    "docs/peers.md",
    "examples/hello-led/README.md",
    "examples/job-runner/README.md",
];

const PUBLIC_SMOKE_SCRIPTS: &[&str] = &["scripts/smoke-ndjson.sh", "scripts/smoke-ws.sh"];

fn public_docs(paths: &[&str]) -> String {
    paths
        .iter()
        .map(|path| format!("\n<!-- {path} -->\n{}", read(path)))
        .collect::<Vec<_>>()
        .join("\n")
}

#[test]
fn caql_docs_mention_new_grammar_and_error_envelope() {
    let s = read("docs/caql.md");
    assert!(s.contains("contains"), "caql.md should mention `contains`");
    assert!(s.contains("exists"), "caql.md should mention `exists`");
    assert!(
        s.contains("kind"),
        "caql.md should mention the canonical `kind` field"
    );
    assert!(
        s.contains("400"),
        "caql.md should describe the 400 error response"
    );
    assert!(
        s.contains("ResourceSnapshot") || s.contains("snapshot"),
        "caql.md should describe the resource query target"
    );
    for required in ["allowedStates", "result", "idempotency", "requiredScopes"] {
        assert!(
            s.contains(required),
            "caql.md should show the richer transition affordance field `{required}`"
        );
    }
}

#[test]
fn resources_docs_mention_resource_actor_contract() {
    let s = read("docs/resources.md");
    assert!(
        s.contains("ResourceSnapshot"),
        "resources.md should reference ResourceSnapshot"
    );
    assert!(
        s.contains("kind"),
        "resources.md should mention the canonical `kind` field"
    );
    assert!(
        s.contains("Resource"),
        "resources.md should mention Resource"
    );
    assert!(s.contains("Actor"), "resources.md should mention Actor");
    assert!(s.contains("Node"), "resources.md should mention Node");
    assert!(
        s.contains("TransitionOutcome"),
        "resources.md should mention TransitionOutcome"
    );
}

#[test]
fn docs_reference_resource_routes_only() {
    let s = public_docs(PUBLIC_DOCS);

    for required in [
        "/resources",
        "/resources/{id}",
        "/resources/{id}/transitions/{transition}",
        "/servers/{name}/resources",
        "/servers/{name}/resources/{id}/transitions/{transition}",
    ] {
        assert!(s.contains(required), "docs should mention `{required}`");
    }

    for old_route in [
        "/servers/{name}/devices",
        "/servers/<name>/devices",
        "/servers/hub/devices",
        "/devices/{id}",
    ] {
        assert!(
            !s.contains(old_route),
            "public docs should not present old resource route `{old_route}`"
        );
    }
}

#[test]
fn crate_docs_show_resource_actor_imports() {
    let s = read("crates/boardwalk/src/lib.rs");
    for required in [
        "Resource",
        "Actor",
        "NodeBuilder",
        "ResourceSnapshot",
        "TransitionOutcome",
        "SlowConsumerPolicy",
    ] {
        assert!(
            s.contains(required),
            "crate docs should mention `{required}`"
        );
    }
}

#[test]
fn docs_describe_reusable_actor_http_runtime() {
    let resources = read("docs/resources.md");
    assert!(
        resources.contains("Boardwalk::new().use_actor"),
        "resources.md should say Boardwalk actor registration is exposed through reusable HTTP"
    );
    assert!(
        resources.contains("use_actor_with_id"),
        "resources.md should mention stable actor ids for the job-runner queue"
    );

    let getting_started = read("docs/getting-started.md");
    assert!(
        getting_started.contains("workspace fixture"),
        "getting-started.md should label boardwalk_mock_led as a workspace-only fixture"
    );
    assert!(
        getting_started.contains("not published"),
        "getting-started.md should not imply boardwalk_mock_led is an external dependency"
    );
    assert!(
        getting_started.contains("reusable Boardwalk HTTP router"),
        "getting-started.md should name the reusable actor HTTP path"
    );

    let peers = read("docs/peers.md");
    assert!(
        peers.contains("Boardwalk::new().use_actor"),
        "peers.md should show actor registration through the Boardwalk builder"
    );
    assert!(
        peers.contains("listen_until_on"),
        "peers.md should document graceful serving on an already-bound listener"
    );
    assert!(
        peers.contains("`NodeBuilder`"),
        "peers.md should explain the relationship to the actor runtime"
    );

    let hello_led = read("examples/hello-led/README.md");
    assert!(
        hello_led.contains("NodeBuilder"),
        "hello-led README should explain the in-process NodeBuilder path"
    );
    assert!(
        hello_led.contains("Boardwalk"),
        "hello-led README should point HTTP users at Boardwalk"
    );

    let job_runner = read("examples/job-runner/README.md");
    assert!(
        job_runner.contains("cargo run -p boardwalk-job-runner-example"),
        "job-runner README should use the real workspace package name"
    );
    assert!(
        job_runner.contains("Boardwalk::new()"),
        "job-runner README should show the reusable Boardwalk builder"
    );
    assert!(
        job_runner.contains("use_actor_with_id"),
        "job-runner README should show stable actor registration"
    );
    assert!(
        job_runner.contains("reusable HTTP, WebSocket, and peer route stack"),
        "job-runner README should describe the final reusable route stack"
    );

    let crate_docs = read("crates/boardwalk/src/lib.rs");
    assert!(
        crate_docs.contains("Common imports for the Resource/Actor surface"),
        "crate rustdoc should introduce the Resource/Actor import surface"
    );
    assert!(
        !crate_docs.contains("older server-adapter exports"),
        "crate rustdoc should not describe transitional root exports"
    );
}

#[test]
fn smoke_scripts_target_long_running_job_runner_example() {
    let s = public_docs(PUBLIC_SMOKE_SCRIPTS);

    assert!(
        s.contains("cargo run -p boardwalk-job-runner-example"),
        "smoke scripts should tell users to run the long-running job-runner example"
    );
    assert!(
        s.contains("/servers/runner"),
        "smoke scripts should probe the job-runner server"
    );
    assert!(
        s.contains("QUEUE_ID=${QUEUE_ID:-queue-default}") && s.contains("/transitions/submit"),
        "smoke scripts should trigger the job queue actor through the resource transition route"
    );
    assert!(
        s.contains("runner/job/*/progress"),
        "WS smoke should subscribe to job-runner progress events"
    );

    for stale in [
        "hello-led",
        "/servers/hub/devices",
        "hub/led",
        "action=turn-on",
    ] {
        assert!(
            !s.contains(stale),
            "smoke scripts should not depend on stale hello-led runtime detail `{stale}`"
        );
    }
}

#[test]
fn docs_do_not_describe_transitional_runtime_adapter_caveats() {
    let s = public_docs(PUBLIC_DOCS);
    for forbidden in [
        "server adapter",
        "server-adapter",
        "legacy adapter",
        "private adapter",
        "example-local HTTP adapter",
        "example-local actor-backed adapter",
    ] {
        assert!(
            !s.contains(forbidden),
            "public docs should not describe transitional runtime caveat `{forbidden}`"
        );
    }
}

#[test]
fn events_docs_show_transition_correlation_and_causation() {
    let s = read("docs/events.md");
    assert!(
        s.contains("correlationId"),
        "events.md should document transition correlationId"
    );
    assert!(
        s.contains("\"correlationId\": \"req-123\""),
        "events.md should show populated transition correlationId"
    );
    assert!(
        s.contains("causationId"),
        "events.md should document transition causationId"
    );
    assert!(
        s.contains("\"causationId\": \""),
        "events.md should show populated transition causationId"
    );
}

#[test]
fn public_docs_have_no_private_planning_terms() {
    let s = public_docs(PUBLIC_DOCS);
    let private_terms = [
        format!("{}bo", "Gum"),
        format!(".{}{}", "gum", "bo"),
        format!("Plan {}", "0003"),
        format!("Task {}", "7.4"),
        format!("{}/", "findings"),
    ];
    for private in private_terms {
        assert!(
            !s.contains(private.as_str()),
            "public docs should not mention private planning term `{private}`"
        );
    }
}

#[test]
fn markdown_docs_do_not_teach_old_identifiers() {
    let s = public_docs(PUBLIC_MARKDOWN_DOCS);
    let old_identifiers = [
        "DeviceConfig",
        "DeviceError",
        "DeviceProxy",
        "ServerHandle",
        "use_device",
        "#[device]",
        "docs/devices",
        "devices.md",
    ];
    for old in old_identifiers {
        assert!(
            !s.contains(old),
            "markdown docs should not teach old scaffolding identifier `{old}`"
        );
    }
}

#[test]
fn docs_event_envelope_documents_envelope_version_and_policies() {
    let s = read("docs/event-envelope.md");
    assert!(s.contains("envelopeVersion"));
    assert!(s.contains("eventId"));
    assert!(s.contains("sequence"));
    assert!(s.contains("SlowConsumerPolicy"));
    assert!(s.contains("Disconnect"));
    assert!(s.contains("DropNewest"));
    assert!(s.contains("stream-gap"));
    assert!(s.contains("broadcast_lag"));
    assert!(
        s.contains("Coalesce { key_path }"),
        "event-envelope.md should describe the shipped Coalesce policy with its `key_path` parameter"
    );
    assert!(
        s.contains("non-coalescible"),
        "event-envelope.md should pin the missing-key contract (envelopes whose key path does not resolve are non-coalescible)"
    );
    assert!(
        !s.contains("intentionally deferred"),
        "event-envelope.md still describes Coalesce as deferred even though it ships now"
    );
}

#[test]
fn docs_events_document_ndjson_replay_and_policy_query() {
    let s = read("docs/events.md");
    for required in [
        "\"stream\"",
        "replay=true",
        "outboundCapacity",
        "slowConsumerPolicy",
        "coalesceKey",
        "node/kind/resource/stream",
    ] {
        assert!(
            s.contains(required),
            "events.md should document NDJSON `{required}`"
        );
    }
}

#[test]
fn docs_events_documents_envelope_fields_and_stream_gap() {
    let s = read("docs/events.md");
    assert!(s.contains("eventId"));
    assert!(s.contains("streamId"));
    assert!(s.contains("\"stream\":\"state\""));
    assert!(s.contains("sequence"));
    assert!(s.contains("stream-gap"));
    assert!(s.contains("slow_consumer"));
    assert!(s.contains("replay=true"));
    assert!(s.contains("slowConsumerPolicy"));
    assert!(s.contains("coalesceKey"));
}
