//! Composition harness for ordered multi-aspect chains.
//!
//! This module supports RQ2 of the ICSE 2027 evaluation: it composes
//! N aspects at one join point in a declared order, records a trace
//! of every advice fire, and surfaces three classes of interaction:
//!
//! 1. **Short-circuit**: an earlier aspect's `around` returned `Err`
//!    before a later aspect saw the join point.
//! 2. **Order sensitivity**: re-ordering the same aspect set produces
//!    different traces (or different outcomes) over the same input.
//! 3. **Outcome divergence**: two orderings produce different `Ok` vs.
//!    `Err` results for the same input.
//!
//! The harness is independent of `AspectRegistry` so it can be driven
//! from criterion benchmarks and unit tests without touching global
//! state.

use aspect_core::{Aspect, AspectError, JoinPoint, Location, ProceedingJoinPoint};
use parking_lot::Mutex;
use std::any::Any;
use std::sync::Arc;

/// One entry in a composition trace.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TraceEntry {
    /// Aspect name as declared in the chain.
    pub aspect: String,
    /// Phase of advice that fired.
    pub phase: Phase,
}

/// Advice phase fired by an aspect within the chain.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Phase {
    /// Outer entry to `around` advice (before delegating inward).
    AroundEnter,
    /// Outer exit from `around` advice (after the inner chain returned).
    AroundExit,
    /// Outer exit from `around` advice where the result is an error.
    AroundError,
    /// Aspect short-circuited the chain by returning `Err` from its `around`
    /// without calling `proceed`.
    ShortCircuit,
}

/// Shared trace buffer that aspects write into.
#[derive(Default, Clone)]
pub struct TraceLog {
    entries: Arc<Mutex<Vec<TraceEntry>>>,
}

impl TraceLog {
    /// Create an empty trace log.
    pub fn new() -> Self {
        Self::default()
    }

    /// Append an entry to the trace.
    pub fn push(&self, entry: TraceEntry) {
        self.entries.lock().push(entry);
    }

    /// Snapshot the current trace.
    pub fn snapshot(&self) -> Vec<TraceEntry> {
        self.entries.lock().clone()
    }

    /// Clear the trace.
    pub fn clear(&self) {
        self.entries.lock().clear();
    }
}

/// One aspect slot in the chain: aspect impl + the name used in traces.
pub struct ChainSlot {
    /// Stable, human-readable name (e.g. "RateLimiter").
    pub name: String,
    /// The aspect implementation.
    pub aspect: Arc<dyn Aspect>,
}

impl ChainSlot {
    /// Create a slot from a name and an aspect.
    pub fn new<A: Aspect + 'static>(name: impl Into<String>, aspect: A) -> Self {
        Self {
            name: name.into(),
            aspect: Arc::new(aspect),
        }
    }

    /// Create a slot from an already-shared aspect.
    pub fn from_arc(name: impl Into<String>, aspect: Arc<dyn Aspect>) -> Self {
        Self {
            name: name.into(),
            aspect,
        }
    }
}

/// An ordered chain of aspects with a shared trace log.
pub struct Composition {
    slots: Vec<ChainSlot>,
    trace: TraceLog,
}

impl Composition {
    /// Build a composition from a vector of slots in the firing order.
    /// The first slot is the outermost aspect.
    pub fn new(slots: Vec<ChainSlot>) -> Self {
        Self {
            slots,
            trace: TraceLog::new(),
        }
    }

    /// Replace the trace log (e.g. to share one log across multiple runs).
    pub fn with_trace(mut self, trace: TraceLog) -> Self {
        self.trace = trace;
        self
    }

    /// Access the trace log.
    pub fn trace(&self) -> &TraceLog {
        &self.trace
    }

    /// Run the chain against a target closure.
    ///
    /// `function_name` is reported on every trace entry's join point so
    /// the same chain can be exercised at multiple sites in one benchmark.
    pub fn run<F, T>(&self, function_name: &'static str, target: F) -> Result<Box<dyn Any>, AspectError>
    where
        F: FnOnce() -> Result<T, AspectError> + 'static,
        T: 'static,
    {
        let jp = JoinPoint {
            function_name,
            module_path: "composition::harness",
            location: Location {
                file: "harness.rs",
                line: 0,
            },
        };

        let pjp = ProceedingJoinPoint::new(move || target().map(|v| Box::new(v) as Box<dyn Any>), jp.clone());
        self.run_inner(pjp, &jp)
    }

    fn run_inner(
        &self,
        mut pjp: ProceedingJoinPoint,
        jp: &JoinPoint,
    ) -> Result<Box<dyn Any>, AspectError> {
        // Build the chain inside-out: the innermost wrap is the last slot,
        // so we iterate in reverse and wrap each layer with a TracingShim.
        for slot in self.slots.iter().rev() {
            let aspect = Arc::clone(&slot.aspect);
            let trace = self.trace.clone();
            let name = slot.name.clone();
            let inner = pjp;
            pjp = ProceedingJoinPoint::new(
                move || {
                    trace.push(TraceEntry {
                        aspect: name.clone(),
                        phase: Phase::AroundEnter,
                    });
                    let result = aspect.around(inner);
                    match &result {
                        Ok(_) => trace.push(TraceEntry {
                            aspect: name.clone(),
                            phase: Phase::AroundExit,
                        }),
                        Err(_) => trace.push(TraceEntry {
                            aspect: name.clone(),
                            phase: Phase::AroundError,
                        }),
                    }
                    result
                },
                jp.clone(),
            );
        }

        pjp.proceed()
    }
}

/// Three classes of interaction detected by comparing two chain runs.
#[derive(Debug, PartialEq, Eq)]
pub enum Interaction {
    /// One aspect's `around` short-circuited; later aspects never fired.
    ShortCircuit {
        /// Name of the aspect that short-circuited.
        culprit: String,
        /// Names of aspects that were declared after the culprit but
        /// never entered their around advice.
        skipped: Vec<String>,
    },
    /// Two orderings produced different traces over the same input.
    OrderSensitive {
        /// Trace from chain A.
        trace_a: Vec<TraceEntry>,
        /// Trace from chain B.
        trace_b: Vec<TraceEntry>,
    },
    /// Two orderings produced different Ok/Err outcomes for the same input.
    OutcomeDivergence {
        /// Whether chain A returned Ok.
        a_ok: bool,
        /// Whether chain B returned Ok.
        b_ok: bool,
    },
}

/// Analyze one trace for short-circuit events relative to the declared chain.
///
/// Returns one `ShortCircuit` interaction per aspect that returned an error
/// from its around advice without all subsequent aspects also firing.
pub fn detect_short_circuits(
    declared: &[String],
    trace: &[TraceEntry],
) -> Vec<Interaction> {
    let mut out = Vec::new();
    for (idx, name) in declared.iter().enumerate() {
        let aspect_errored = trace
            .iter()
            .any(|e| e.aspect == *name && e.phase == Phase::AroundError);
        if !aspect_errored {
            continue;
        }
        let skipped: Vec<String> = declared
            .iter()
            .skip(idx + 1)
            .filter(|inner| {
                !trace
                    .iter()
                    .any(|e| e.aspect == **inner && e.phase == Phase::AroundEnter)
            })
            .cloned()
            .collect();
        if !skipped.is_empty() {
            out.push(Interaction::ShortCircuit {
                culprit: name.clone(),
                skipped,
            });
        }
    }
    out
}

/// Compare two runs of the same aspect set in different orders.
pub fn compare_orderings(
    trace_a: &[TraceEntry],
    outcome_a_ok: bool,
    trace_b: &[TraceEntry],
    outcome_b_ok: bool,
) -> Vec<Interaction> {
    let mut out = Vec::new();
    if outcome_a_ok != outcome_b_ok {
        out.push(Interaction::OutcomeDivergence {
            a_ok: outcome_a_ok,
            b_ok: outcome_b_ok,
        });
    }
    if trace_a != trace_b {
        out.push(Interaction::OrderSensitive {
            trace_a: trace_a.to_vec(),
            trace_b: trace_b.to_vec(),
        });
    }
    out
}

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

    #[derive(Default)]
    struct Pass;
    impl Aspect for Pass {
        fn around(&self, pjp: ProceedingJoinPoint) -> Result<Box<dyn Any>, AspectError> {
            pjp.proceed()
        }
    }

    struct Deny;
    impl Aspect for Deny {
        fn around(&self, _pjp: ProceedingJoinPoint) -> Result<Box<dyn Any>, AspectError> {
            Err(AspectError::execution("denied"))
        }
    }

    #[test]
    fn two_pass_chain_records_nested_traces() {
        let chain = Composition::new(vec![
            ChainSlot::new("Outer", Pass),
            ChainSlot::new("Inner", Pass),
        ]);
        let result = chain.run("target", || Ok::<i32, AspectError>(7));
        assert!(result.is_ok());

        let trace = chain.trace().snapshot();
        let phases: Vec<_> = trace.iter().map(|t| (t.aspect.as_str(), t.phase)).collect();
        assert_eq!(
            phases,
            vec![
                ("Outer", Phase::AroundEnter),
                ("Inner", Phase::AroundEnter),
                ("Inner", Phase::AroundExit),
                ("Outer", Phase::AroundExit),
            ]
        );
    }

    #[test]
    fn deny_short_circuits_inner_aspects() {
        let chain = Composition::new(vec![
            ChainSlot::new("Gate", Deny),
            ChainSlot::new("Skipped", Pass),
        ]);
        let result = chain.run("target", || Ok::<i32, AspectError>(0));
        assert!(result.is_err());

        let trace = chain.trace().snapshot();
        let declared = vec!["Gate".to_string(), "Skipped".to_string()];
        let interactions = detect_short_circuits(&declared, &trace);
        assert_eq!(interactions.len(), 1);
        match &interactions[0] {
            Interaction::ShortCircuit { culprit, skipped } => {
                assert_eq!(culprit, "Gate");
                assert_eq!(skipped, &vec!["Skipped".to_string()]);
            }
            other => panic!("unexpected interaction: {:?}", other),
        }
    }

    #[test]
    fn reordering_a_passing_chain_preserves_outcome() {
        let chain_ab = Composition::new(vec![
            ChainSlot::new("A", Pass),
            ChainSlot::new("B", Pass),
        ]);
        let chain_ba = Composition::new(vec![
            ChainSlot::new("B", Pass),
            ChainSlot::new("A", Pass),
        ]);

        let r1 = chain_ab.run("t", || Ok::<i32, AspectError>(1));
        let r2 = chain_ba.run("t", || Ok::<i32, AspectError>(1));
        assert_eq!(r1.is_ok(), r2.is_ok());

        let interactions = compare_orderings(
            &chain_ab.trace().snapshot(),
            r1.is_ok(),
            &chain_ba.trace().snapshot(),
            r2.is_ok(),
        );
        // Traces differ because aspect names differ at each layer, but
        // outcomes agree — only OrderSensitive should fire, not OutcomeDivergence.
        assert!(interactions.iter().all(|i| !matches!(i, Interaction::OutcomeDivergence { .. })));
    }

    #[test]
    fn deny_then_pass_vs_pass_then_deny_diverges() {
        // Both orderings should err because Deny always denies, but their
        // traces differ in *which* aspects fire before the denial.
        let deny_first = Composition::new(vec![
            ChainSlot::new("D", Deny),
            ChainSlot::new("P", Pass),
        ]);
        let pass_first = Composition::new(vec![
            ChainSlot::new("P", Pass),
            ChainSlot::new("D", Deny),
        ]);

        let r1 = deny_first.run("t", || Ok::<i32, AspectError>(1));
        let r2 = pass_first.run("t", || Ok::<i32, AspectError>(1));

        assert!(r1.is_err() && r2.is_err());

        let interactions = compare_orderings(
            &deny_first.trace().snapshot(),
            r1.is_ok(),
            &pass_first.trace().snapshot(),
            r2.is_ok(),
        );
        assert!(interactions
            .iter()
            .any(|i| matches!(i, Interaction::OrderSensitive { .. })));
    }
}
