//! Post-parse reference and path resolution applied to a [`ResolvedSuite`].
//!
//! These run after [`super::parse`]: rebasing relative `cassette:` paths
//! against the suite directory, and checking that every `server:` reference
//! resolves to a declared server. They live here rather than in `mod.rs` to
//! keep that module under the per-file line cap.

use std::collections::BTreeMap;
use std::path::Path;

use super::types::{ResolvedSuite, ServerSpec};
use super::ParseError;

/// Resolve relative `cassette:` paths against `base_dir` (the directory of
/// the suite file) so a suite replays identically from any working
/// directory, matching how `imports:` resolve. Absolute paths are left
/// untouched. The run handlers call this after [`super::parse`], which has
/// no knowledge of the file location.
pub fn rebase_cassette_paths(servers: &mut BTreeMap<String, ServerSpec>, base_dir: &Path) {
    for spec in servers.values_mut() {
        if let ServerSpec::Cassette { path, .. } = spec {
            if Path::new(path.as_str()).is_relative() {
                *path = base_dir.join(&*path).to_string_lossy().into_owned();
            }
        }
    }
}

/// Check that every `server:` reference in `suite` resolves to a declared
/// server, returning [`ParseError::UnknownServerRef`] for the first miss.
///
/// Composition graphs and `inject_error`/`inject` references are already
/// validated during [`super::parse`]; this covers tool/resource/prompt
/// tests, compliance tests, agent tests, and `tool_quality` checks. The
/// run handlers call it
/// right after `parse` so a typo fails before any server is connected,
/// instead of partway through the run.
pub fn validate_server_refs(suite: &ResolvedSuite) -> Result<(), ParseError> {
    let known: std::collections::BTreeSet<&str> =
        suite.servers.keys().map(String::as_str).collect();
    let ensure = |referrer: &str, server: &str| -> Result<(), ParseError> {
        if known.contains(server) {
            return Ok(());
        }
        Err(ParseError::UnknownServerRef {
            referrer: referrer.to_string(),
            server: server.to_string(),
            known: known.iter().copied().collect::<Vec<_>>().join(", "),
        })
    };
    for t in &suite.tests {
        ensure(&t.name, &t.server)?;
    }
    for c in &suite.compliance_tests {
        ensure(&c.name, &c.server)?;
    }
    for a in &suite.agents {
        for s in &a.servers {
            ensure(&a.name, s)?;
        }
    }
    for c in &suite.tool_quality {
        ensure(&format!("tool_quality `{}`", c.server), &c.server)?;
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::suite::parse;
    use serde_yaml::{from_str, Value as YamlValue};

    #[test]
    fn rebase_makes_relative_cassette_paths_suite_relative() {
        let mut servers = BTreeMap::new();
        servers.insert(
            "replay".to_string(),
            ServerSpec::Cassette {
                path: "recordings/api.json".to_string(),
                refresh_command: None,
                refresh_url: None,
            },
        );
        servers.insert(
            "absolute".to_string(),
            ServerSpec::Cassette {
                path: "/tmp/fixed.json".to_string(),
                refresh_command: None,
                refresh_url: None,
            },
        );
        rebase_cassette_paths(&mut servers, Path::new("suites/api"));
        match &servers["replay"] {
            // Relative path is joined to the suite dir (separator-agnostic).
            ServerSpec::Cassette { path, .. } => assert_eq!(
                Path::new(path),
                Path::new("suites/api").join("recordings/api.json")
            ),
            other => panic!("expected cassette, got {other:?}"),
        }
        match &servers["absolute"] {
            // Absolute path is untouched.
            ServerSpec::Cassette { path, .. } => assert_eq!(path, "/tmp/fixed.json"),
            other => panic!("expected cassette, got {other:?}"),
        }
    }

    #[test]
    fn validate_server_refs_flags_a_typo_and_lists_declared_servers() {
        let yaml: YamlValue = from_str(
            r#"
servers:
  fs:
    command: ["node", "./server.js"]
tools:
  - name: echoes input
    server: fss
    tool: echo
    expect:
      - target: result.isError
        matcher:
          exact: false
"#,
        )
        .unwrap();
        let suite = parse(&yaml).unwrap();
        match validate_server_refs(&suite) {
            Err(ParseError::UnknownServerRef {
                referrer,
                server,
                known,
            }) => {
                assert_eq!(referrer, "echoes input");
                assert_eq!(server, "fss");
                assert!(known.contains("fs"), "lists declared servers: {known}");
            }
            other => panic!("expected UnknownServerRef, got {other:?}"),
        }
    }

    #[test]
    fn validate_server_refs_flags_a_compliance_test_with_an_unknown_server() {
        let yaml: YamlValue = from_str(
            r#"
servers:
  fs:
    command: ["node", "./server.js"]
compliance:
  - name: PROTO-001 initialize handshake
    server: missing
    check: "initialize"
"#,
        )
        .unwrap();
        let suite = parse(&yaml).unwrap();
        match validate_server_refs(&suite) {
            Err(ParseError::UnknownServerRef {
                referrer, server, ..
            }) => {
                assert_eq!(referrer, "PROTO-001 initialize handshake");
                assert_eq!(server, "missing");
            }
            other => panic!("expected UnknownServerRef, got {other:?}"),
        }
    }

    #[test]
    fn validate_server_refs_accepts_a_declared_server() {
        let yaml: YamlValue = from_str(
            r#"
servers:
  fs:
    command: ["node", "./server.js"]
tools:
  - name: ok
    server: fs
    tool: echo
    expect:
      - target: result.isError
        matcher:
          exact: false
"#,
        )
        .unwrap();
        let suite = parse(&yaml).unwrap();
        assert!(validate_server_refs(&suite).is_ok());
    }
}
