//! Boundary checks for foreign plugin manifests.
//!
//! The fixture in this test is synthetic and host-neutral. It represents
//! manifest vocabulary owned by an external plugin ecosystem, not Lifeloop.
//! Lifeloop must not treat such files as adapter manifests or registry entries.

use std::fs;
use std::path::{Path, PathBuf};

use lifeloop::{AdapterManifest, lookup_manifest, manifest_registry};
use serde_json::Value;

const FIXTURE_ROOT: &[&str] = &["tests", "fixtures", "foreign_manifests"];

const CLIENT_SEMANTIC_TERMS: &[&str] = &[
    "ccd",
    "rlm",
    "reforge",
    "fixity",
    "memory",
    "recall",
    "promotion",
    "governance",
];

#[test]
fn foreign_manifest_fixtures_stay_synthetic_host_neutral_and_outside_lifeloop_contract() {
    let fixture_paths = fixture_paths();
    assert!(
        !fixture_paths.is_empty(),
        "foreign manifest boundary test must cover at least one fixture"
    );

    for path in fixture_paths {
        let (raw, manifest) = load_fixture(&path);

        assert_synthetic_file_name(&path);
        assert_synthetic_provenance(&manifest, &path);
        assert_foreign_schema(&manifest, &path);
        assert_not_registered_adapter_name(&manifest, &path);
        assert_not_lifeloop_adapter_manifest(&manifest, &path);
        assert_host_neutral(&raw, &path);
        assert_client_semantics_neutral(&raw, &path);
    }
}

fn fixture_root() -> PathBuf {
    let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    for part in FIXTURE_ROOT {
        path.push(part);
    }
    path
}

fn fixture_paths() -> Vec<PathBuf> {
    let root = fixture_root();
    let mut paths: Vec<_> = fs::read_dir(&root)
        .unwrap_or_else(|err| panic!("read {}: {err}", root.display()))
        .map(|entry| {
            entry
                .unwrap_or_else(|err| panic!("read entry in {}: {err}", root.display()))
                .path()
        })
        .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("json"))
        .collect();
    paths.sort();
    paths
}

fn assert_synthetic_file_name(path: &Path) {
    let file_name = path
        .file_name()
        .and_then(|name| name.to_str())
        .expect("fixture path must end in a UTF-8 file name");
    assert!(
        file_name.starts_with("synthetic-"),
        "{} must use a `synthetic-` file name",
        path.display()
    );
}

fn assert_synthetic_provenance(manifest: &Value, path: &Path) {
    assert_eq!(
        manifest["provenance"]["kind"],
        "synthetic",
        "{} must declare provenance.kind=synthetic",
        path.display()
    );
}

fn assert_foreign_schema(manifest: &Value, path: &Path) {
    let schema = manifest["schema"]
        .as_str()
        .unwrap_or_else(|| panic!("{} must declare a string schema", path.display()));
    assert!(
        !schema.starts_with("lifeloop."),
        "{} must not use a Lifeloop-owned schema id",
        path.display()
    );
}

fn assert_not_registered_adapter_name(manifest: &Value, path: &Path) {
    let plugin_name = manifest["name"]
        .as_str()
        .unwrap_or_else(|| panic!("{} must declare a string plugin name", path.display()));
    assert!(
        lookup_manifest(plugin_name).is_none(),
        "{} plugin name `{plugin_name}` must not resolve as a Lifeloop adapter",
        path.display()
    );
}

fn assert_not_lifeloop_adapter_manifest(manifest: &Value, path: &Path) {
    let parsed_as_lifeloop_manifest = serde_json::from_value::<AdapterManifest>(manifest.clone());
    assert!(
        parsed_as_lifeloop_manifest.is_err(),
        "{} must not deserialize as Lifeloop AdapterManifest",
        path.display()
    );
}

fn assert_host_neutral(raw: &str, path: &Path) {
    let raw_lower = raw.to_ascii_lowercase();
    for term in registered_adapter_terms() {
        assert!(
            !raw_lower.contains(&term),
            "{} must not mention registered adapter term `{term}`",
            path.display()
        );
    }
}

fn registered_adapter_terms() -> Vec<String> {
    let mut terms = Vec::new();
    for entry in manifest_registry() {
        for term in [entry.manifest.adapter_id, entry.manifest.display_name] {
            let normalized = term.to_ascii_lowercase();
            if !terms.contains(&normalized) {
                terms.push(normalized);
            }
        }
    }
    terms
}

fn assert_client_semantics_neutral(raw: &str, path: &Path) {
    let raw_lower = raw.to_ascii_lowercase();
    for term in CLIENT_SEMANTIC_TERMS {
        assert!(
            !raw_lower.contains(term),
            "{} must not encode client semantic term `{term}`",
            path.display()
        );
    }
}

fn load_fixture(path: &Path) -> (String, Value) {
    let raw =
        fs::read_to_string(path).unwrap_or_else(|err| panic!("read {}: {err}", path.display()));
    let value =
        serde_json::from_str(&raw).unwrap_or_else(|err| panic!("parse {}: {err}", path.display()));
    (raw, value)
}
