Documentation
use crate::fixture::Fixture;
use diwe::config::{Configuration, Patterns, SchemaBinding};
use rmcp::model::ErrorData;
use rmcp::ServiceError;
use serde_json::json;
use std::collections::HashMap;
use std::fs::{create_dir_all, read_to_string, write};
use tempfile::TempDir;

const CLEAN: &str = "# Summary\n\n# Tasks\n";

const PERSON_SCHEMA: &str =
    "sections:\n  - header: { const: Summary }\n  - header: { const: Tasks }\n";

#[tokio::test]
async fn update_violating_change_is_rejected_and_not_written() {
    let dir = setup(PERSON_SCHEMA);
    let base = dir.path();
    let f = Fixture::with_path(base.to_str().unwrap(), config("person", "docs/**")).await;

    let err = f
        .try_call_tool(
            "iwe_update",
            json!({ "key": "docs/one", "content": "# Summary\n" }),
        )
        .await
        .unwrap_err();

    let error = mcp_error(err);
    assert_eq!(
        error.message,
        "schema validation failed; change rejected:\ndocs/one: required section \"Tasks\" is missing\n"
    );
    assert_eq!(
        error.data,
        Some(json!({
            "violations": [
                {
                    "key": "docs/one",
                    "schema": "person",
                    "violations": [
                        {
                            "breadcrumb": [],
                            "message": "required section \"Tasks\" is missing",
                            "hint": null,
                            "schemaPath": "/sections/1/minContains",
                            "keyword": "minContains"
                        }
                    ]
                }
            ]
        }))
    );

    assert_eq!(read_to_string(base.join("docs/one.md")).unwrap(), CLEAN);
}

#[tokio::test]
async fn update_clean_change_is_written() {
    let dir = setup(PERSON_SCHEMA);
    let base = dir.path();
    let f = Fixture::with_path(base.to_str().unwrap(), config("person", "docs/**")).await;

    let new_content = "# Summary\n\nmore\n\n# Tasks\n";
    f.call_tool(
        "iwe_update",
        json!({ "key": "docs/one", "content": new_content }),
    )
    .await;

    assert_eq!(
        read_to_string(base.join("docs/one.md")).unwrap(),
        new_content
    );
}

#[tokio::test]
async fn create_violating_document_is_rejected_and_not_written() {
    let dir = setup(PERSON_SCHEMA);
    let base = dir.path();
    let f = Fixture::with_path(base.to_str().unwrap(), config("person", "docs/**")).await;

    let err = f
        .try_call_tool("iwe_create", json!({ "key": "docs/ada", "title": "Ada" }))
        .await
        .unwrap_err();

    let error = mcp_error(err);
    assert_eq!(
        error.message,
        "schema validation failed; change rejected:\ndocs/ada: required section \"Summary\" is missing\ndocs/ada: required section \"Tasks\" is missing\n"
    );
    assert!(!base.join("docs/ada.md").exists());
}

#[tokio::test]
async fn missing_schema_file_is_a_configuration_error() {
    let dir = TempDir::new().unwrap();
    let base = dir.path();
    create_dir_all(base.join(".iwe/schemas")).unwrap();
    create_dir_all(base.join("docs")).unwrap();
    write(base.join("docs/one.md"), CLEAN).unwrap();
    let f = Fixture::with_path(base.to_str().unwrap(), config("ghost", "docs/**")).await;

    let err = f
        .try_call_tool(
            "iwe_update",
            json!({ "key": "docs/one", "content": "# Summary\n" }),
        )
        .await
        .unwrap_err();

    let error = mcp_error(err);
    assert_eq!(
        error.message,
        "schema configuration error: schema 'ghost': .iwe/schemas/ghost.yaml not found"
    );
    assert_eq!(read_to_string(base.join("docs/one.md")).unwrap(), CLEAN);
}

#[tokio::test]
async fn normalize_is_not_gated_by_a_pre_existing_violation() {
    let dir = TempDir::new().unwrap();
    let base = dir.path();
    create_dir_all(base.join(".iwe/schemas")).unwrap();
    write(base.join(".iwe/schemas/person.yaml"), PERSON_SCHEMA).unwrap();
    create_dir_all(base.join("docs")).unwrap();
    write(base.join("docs/one.md"), "# Summary\n").unwrap();
    let f = Fixture::with_path(base.to_str().unwrap(), config("person", "docs/**")).await;

    let result = f.call_tool("iwe_normalize", json!({})).await;
    let output = Fixture::result_json(&result);

    assert_eq!(output["total"], 1);
}

fn setup(schema: &str) -> TempDir {
    let dir = TempDir::new().unwrap();
    let base = dir.path();
    create_dir_all(base.join(".iwe/schemas")).unwrap();
    write(base.join(".iwe/schemas/person.yaml"), schema).unwrap();
    create_dir_all(base.join("docs")).unwrap();
    write(base.join("docs/one.md"), CLEAN).unwrap();
    dir
}

fn config(name: &str, pattern: &str) -> Configuration {
    let mut schemas = HashMap::new();
    schemas.insert(
        name.to_string(),
        SchemaBinding {
            r#match: Patterns::One(pattern.to_string()),
        },
    );
    Configuration {
        schemas,
        ..Default::default()
    }
}

fn mcp_error(err: ServiceError) -> ErrorData {
    match err {
        ServiceError::McpError(error) => error,
        other => panic!("expected McpError, got: {other:?}"),
    }
}