//! Drift-watch state: the schema snapshot plus golden canary responses that a
//! scheduled run compares against a saved baseline.
//!
//! Schema drift (a renamed or removed parameter) is caught by the existing
//! catalog diff. Behavioral drift (a canary that used to return rows now
//! returns nothing) is caught by fingerprinting the canary responses and
//! comparing the fingerprints, value-tolerantly, against the baseline. A
//! [`DriftState`] bundles both so one file is the whole watched contract.

use std::collections::BTreeMap;

use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::diff::{error_signature, fingerprint_response, ToolResponseSample};

/// One golden canary: a tool call and the response it produced.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CanarySample {
    /// Tool name called.
    pub tool: String,
    /// Arguments passed to the call (diagnostic; the response is what drifts).
    #[serde(default)]
    pub args: Value,
    /// The `tools/call` result the server returned.
    pub response: Value,
    /// Whether the server flagged the result as an error.
    #[serde(default)]
    pub error: bool,
}

/// A captured drift state: the tool catalog plus the canary responses.
///
/// Both a baseline (recorded once) and a current capture (each scheduled run)
/// are `DriftState` values; [`super::compare()`] diffs the two.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DriftState {
    /// State-file schema version.
    #[serde(default = "default_version")]
    pub version: String,
    /// ISO-8601 capture time. Diagnostic only.
    #[serde(default)]
    pub generated_at: String,
    /// Label for the watched server (a URL, a command, or a human name).
    #[serde(default)]
    pub server: String,
    /// The `tools/list` result (an object with a `tools` array), used for the
    /// schema diff.
    pub schema: Value,
    /// The golden canary calls and their recorded responses.
    #[serde(default)]
    pub canaries: Vec<CanarySample>,
}

fn default_version() -> String {
    "0.1".to_string()
}

impl DriftState {
    /// Fingerprint every canary response into the value-tolerant
    /// [`ToolResponseSample`] map the behavioral diff consumes, keyed by tool
    /// name. An errored canary contributes an error signature; a successful
    /// one contributes a structural fingerprint.
    pub fn canary_fingerprints(&self) -> BTreeMap<String, ToolResponseSample> {
        let mut map: BTreeMap<String, ToolResponseSample> = BTreeMap::new();
        for canary in &self.canaries {
            let entry = map
                .entry(canary.tool.clone())
                .or_insert(ToolResponseSample {
                    success: None,
                    errors: Vec::new(),
                });
            if canary.error {
                let body = serde_json::to_string(&canary.response).unwrap_or_default();
                entry.errors.push(error_signature(&body));
            } else {
                entry.success = Some(fingerprint_response(&canary.response));
            }
        }
        map
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn fingerprints_a_successful_canary() {
        let state = DriftState {
            version: "0.1".into(),
            generated_at: String::new(),
            server: "demo".into(),
            schema: json!({ "tools": [] }),
            canaries: vec![CanarySample {
                tool: "search".into(),
                args: json!({ "q": "ping" }),
                response: json!({ "content": [{ "type": "text", "text": "row" }] }),
                error: false,
            }],
        };
        let fps = state.canary_fingerprints();
        assert!(fps["search"].success.is_some());
        assert!(fps["search"].errors.is_empty());
    }

    #[test]
    fn fingerprints_an_errored_canary() {
        let state = DriftState {
            version: "0.1".into(),
            generated_at: String::new(),
            server: "demo".into(),
            schema: json!({ "tools": [] }),
            canaries: vec![CanarySample {
                tool: "search".into(),
                args: json!({}),
                response: json!({ "message": "not found: item 7" }),
                error: true,
            }],
        };
        let fps = state.canary_fingerprints();
        assert!(fps["search"].success.is_none());
        assert_eq!(fps["search"].errors.len(), 1);
    }
}
