//! Two structural trajectory axes scored from a recorded trace with no model
//! in the loop (WOR-1368): dependency-satisfaction and order-satisfaction.
//!
//! Where [`super::validate_trace`] asks "did the right calls happen?" and
//! [`super::score_golden_path`] asks "did they happen efficiently?", these axes
//! ask "did they happen in a structurally valid order?". A real multi-step task
//! has data and control dependencies: a call that consumes another's output must
//! come after it, and ordered steps must run in order. Both are checked
//! positionally against the recorded call sequence.
//!
//! Scope note: only the POSITIONAL form of dependency is checkable from an agent
//! trace. The envelope carries each call's `args`, but nothing links a downstream
//! call's argument value back to a specific upstream result (the model receives
//! the result as context and independently writes the next call's args). True
//! data-flow provenance lives only in the composition DAG's `${id.field}`
//! substitution, which agent runs do not go through. So the suite declares the
//! dependency edges explicitly and this scorer enforces that the consumer call
//! occurred after the producer call.

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

/// One declared dependency edge: the `consumer` call must occur after the
/// `producer` call in the recorded trace.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DependencyEdge {
    /// Tool name of the call whose output is consumed.
    pub producer: String,
    /// Tool name of the call that must occur after the producer.
    pub consumer: String,
}

impl DependencyEdge {
    /// Build an edge from a producer and consumer tool name. Kept so call
    /// sites and tests share one construction path.
    pub fn new(producer: impl Into<String>, consumer: impl Into<String>) -> Self {
        Self {
            producer: producer.into(),
            consumer: consumer.into(),
        }
    }
}

/// The two structural axis scores, each an integer percent in `0..=100`.
///
/// Serializable so the report can carry both axes next to the trajectory
/// verdict. A percent rather than a bool so a partially-satisfied set of
/// constraints still reads as a number the standard matchers gate.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct AxesScore {
    /// Percent of declared dependency edges where the consumer call occurred
    /// after the last producer call. 100 when no edges are declared.
    pub dependency_satisfaction: u32,
    /// Percent of declared ordered pairs where the first call precedes the
    /// second in the recorded sequence. 100 when no pairs are declared.
    pub order_satisfaction: u32,
}

/// Score a recorded trajectory against declared dependency and order
/// constraints. Pure, no I/O, no model call.
///
/// `actual_tool_calls` is the recorded `tool_calls` slice (each a JSON object
/// with at least a `name`), in dispatch order. See the module doc for the
/// positional semantics of each axis.
pub fn score_trajectory_axes(
    actual_tool_calls: &[Value],
    dependencies: &[DependencyEdge],
    order: &[(String, String)],
) -> AxesScore {
    let names: Vec<&str> = actual_tool_calls.iter().map(name_of).collect();
    AxesScore {
        dependency_satisfaction: dependency_satisfaction(&names, dependencies),
        order_satisfaction: order_satisfaction(&names, order),
    }
}

/// The recorded tool name at one call, or the empty string when absent.
fn name_of(call: &Value) -> &str {
    call.get("name").and_then(Value::as_str).unwrap_or("")
}

/// Integer percent of dependency edges satisfied.
///
/// An edge `(producer, consumer)` is satisfied when the last recorded
/// `consumer` call has a strictly higher index than the last recorded
/// `producer` call. If either name never appears, the edge is unsatisfied (the
/// consumer cannot have used an output that was never produced). An empty edge
/// set is vacuously fully satisfied.
fn dependency_satisfaction(names: &[&str], dependencies: &[DependencyEdge]) -> u32 {
    if dependencies.is_empty() {
        return 100;
    }
    let satisfied = dependencies
        .iter()
        .filter(|edge| {
            let producer = names.iter().rposition(|&n| n == edge.producer);
            let consumer = names.iter().rposition(|&n| n == edge.consumer);
            matches!((producer, consumer), (Some(p), Some(c)) if c > p)
        })
        .count();
    (satisfied * 100 / dependencies.len()) as u32
}

/// Integer percent of ordered pairs satisfied.
///
/// A pair `(first, second)` is satisfied when the earliest recorded `first`
/// call precedes the latest recorded `second` call, so an in-order occurrence
/// exists even when the names also appear elsewhere. If either name is absent
/// the pair is unsatisfied. An empty order set is vacuously fully satisfied.
fn order_satisfaction(names: &[&str], order: &[(String, String)]) -> u32 {
    if order.is_empty() {
        return 100;
    }
    let satisfied = order
        .iter()
        .filter(|(first, second)| {
            let first_pos = names.iter().position(|&n| n == first.as_str());
            let second_pos = names.iter().rposition(|&n| n == second.as_str());
            matches!((first_pos, second_pos), (Some(f), Some(s)) if f < s)
        })
        .count();
    (satisfied * 100 / order.len()) as u32
}

/// Build the targets object the trajectory-axes assertions resolve against:
/// `{ "trajectory": { "dependency_satisfaction": <num>,
/// "order_satisfaction": <num> } }`, so a `trajectory.dependency_satisfaction`
/// dot-path resolves directly the same way the other agent gates do.
pub fn trajectory_axes_metrics(score: &AxesScore) -> Value {
    serde_json::json!({
        "trajectory": {
            "dependency_satisfaction": score.dependency_satisfaction,
            "order_satisfaction": score.order_satisfaction,
        }
    })
}

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

    fn calls(names: &[&str]) -> Vec<Value> {
        names.iter().map(|n| json!({ "name": n })).collect()
    }

    #[test]
    fn empty_constraints_are_vacuously_satisfied() {
        let score = score_trajectory_axes(&calls(&["a", "b"]), &[], &[]);
        assert_eq!(score.dependency_satisfaction, 100);
        assert_eq!(score.order_satisfaction, 100);
    }

    #[test]
    fn dependency_satisfied_when_consumer_follows_producer() {
        let deps = vec![DependencyEdge::new("search", "fetch")];
        let score = score_trajectory_axes(&calls(&["search", "fetch"]), &deps, &[]);
        assert_eq!(score.dependency_satisfaction, 100);
    }

    #[test]
    fn dependency_unsatisfied_when_consumer_precedes_producer() {
        let deps = vec![DependencyEdge::new("search", "fetch")];
        let score = score_trajectory_axes(&calls(&["fetch", "search"]), &deps, &[]);
        assert_eq!(score.dependency_satisfaction, 0);
    }

    #[test]
    fn dependency_unsatisfied_when_a_call_is_missing() {
        let deps = vec![DependencyEdge::new("search", "fetch")];
        let score = score_trajectory_axes(&calls(&["search"]), &deps, &[]);
        assert_eq!(score.dependency_satisfaction, 0);
    }

    #[test]
    fn dependency_partial_is_a_percent() {
        let deps = vec![
            DependencyEdge::new("a", "b"), // satisfied
            DependencyEdge::new("c", "d"), // d before c: unsatisfied
        ];
        let score = score_trajectory_axes(&calls(&["a", "b", "d", "c"]), &deps, &[]);
        assert_eq!(score.dependency_satisfaction, 50);
    }

    #[test]
    fn order_satisfied_when_first_precedes_second() {
        let order = vec![("plan".to_string(), "act".to_string())];
        let score = score_trajectory_axes(&calls(&["plan", "act"]), &[], &order);
        assert_eq!(score.order_satisfaction, 100);
    }

    #[test]
    fn order_unsatisfied_when_second_precedes_first() {
        let order = vec![("plan".to_string(), "act".to_string())];
        let score = score_trajectory_axes(&calls(&["act", "plan"]), &[], &order);
        assert_eq!(score.order_satisfaction, 0);
    }

    #[test]
    fn order_tolerates_interleaved_extras() {
        let order = vec![("plan".to_string(), "act".to_string())];
        // plan ... noise ... act: still in order.
        let score = score_trajectory_axes(&calls(&["plan", "noise", "act"]), &[], &order);
        assert_eq!(score.order_satisfaction, 100);
    }

    #[test]
    fn both_axes_score_independently() {
        let deps = vec![DependencyEdge::new("search", "fetch")];
        let order = vec![("fetch".to_string(), "search".to_string())]; // wrong order
        let score = score_trajectory_axes(&calls(&["search", "fetch"]), &deps, &order);
        assert_eq!(score.dependency_satisfaction, 100);
        assert_eq!(score.order_satisfaction, 0);
    }

    #[test]
    fn metrics_object_exposes_both_targets() {
        use crate::executor::resolve_target;
        let score = AxesScore {
            dependency_satisfaction: 75,
            order_satisfaction: 50,
        };
        let obj = trajectory_axes_metrics(&score);
        assert_eq!(
            resolve_target(&obj, "trajectory.dependency_satisfaction"),
            json!(75)
        );
        assert_eq!(
            resolve_target(&obj, "trajectory.order_satisfaction"),
            json!(50)
        );
    }
}
