use std::{
    fs,
    path::{Path, PathBuf},
    sync::atomic::{AtomicUsize, Ordering},
};
use tb_vm::project_io::{identity, relative_path, Content, Manifest, SourceLoader};
struct Temp(PathBuf);
impl Temp {
    fn new() -> Self {
        static N: AtomicUsize = AtomicUsize::new(0);
        let p = std::env::temp_dir().join(format!(
            "tb-project-io-{}-{}",
            std::process::id(),
            N.fetch_add(1, Ordering::Relaxed)
        ));
        fs::create_dir_all(&p).unwrap();
        Self(p.canonicalize().unwrap())
    }
    fn write(&self, name: &str, text: &str) -> PathBuf {
        let p = self.0.join(name);
        fs::create_dir_all(p.parent().unwrap()).unwrap();
        fs::write(&p, text).unwrap();
        p
    }
}
impl Drop for Temp {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.0);
    }
}
#[test]
fn overlays_keep_original_files_and_distinct_module_contexts() {
    let t = Temp::new();
    let p = t.write("app.mak", "a.bas\nb.bas\n");
    t.write("a.bas", "'$INCLUDE: 'common.bi'\nPRINT n\nEND\n");
    t.write("b.bas", "'$INCLUDE: 'common.bi'\nSUB Test\nEND SUB\n");
    let include = t.write("common.bi", "CONST n=1\n");
    let mut loader = SourceLoader::default();
    loader
        .insert(&include, Content::Text("CONST n=7\n".into()))
        .unwrap();
    let input = loader.load(&p).unwrap();
    let mut origins = Vec::new();
    for (i, u) in input.units.iter().enumerate() {
        let (_, errors) = u.parse(i as u16, &mut origins);
        assert!(errors.is_empty());
        assert!(u
            .segments
            .iter()
            .any(|s| s.text == "CONST n=7\n" && s.first_line == 1));
    }
    let positions: Vec<_> = origins
        .iter()
        .filter(|o| Path::new(&o.path).file_name().unwrap() == "common.bi")
        .collect();
    assert_eq!(positions.len(), 2);
    assert_ne!(positions[0].module, positions[1].module);
    assert_eq!(fs::read_to_string(include).unwrap(), "CONST n=1\n");
}
#[test]
fn relative_include_precedes_search_paths_and_virtual_files_are_resolved() {
    let t = Temp::new();
    let main = t.write("src/main.bas", "'$INCLUDE: 'Mixed Name.bi'\n");
    t.write("library/mixed name.bi", "PRINT 8\n");
    let mut loader = SourceLoader::default();
    loader.include_paths.push(t.0.join("library"));
    assert!(loader.load(&main).unwrap().units[0]
        .segments
        .iter()
        .any(|s| s.text == "PRINT 8\n"));
    loader
        .insert(
            &t.0.join("src/MIXED NAME.BI"),
            Content::Text("PRINT 9\n".into()),
        )
        .unwrap();
    assert!(loader.load(&main).unwrap().units[0]
        .segments
        .iter()
        .any(|s| s.text == "PRINT 9\n"));
    let virtual_main = t.0.join("virtual.bas");
    loader
        .insert(&virtual_main, Content::Text("END\n".into()))
        .unwrap();
    assert_eq!(loader.load(&virtual_main).unwrap().units[0].name, "VIRTUAL");
    assert!(!virtual_main.exists());
}
#[test]
fn cycle_missing_include_and_literal_directives_are_distinguished() {
    let t = Temp::new();
    let main = t.write("main.bas", "'$INCLUDE: 'outer.bi'\n");
    t.write("outer.bi", "'$INCLUDE: 'inner.bi'\n");
    t.write("inner.bi", "'$INCLUDE: 'outer.bi'\n");
    let loader = SourceLoader::default();
    let error = loader.load(&main).unwrap_err();
    assert!(
        error.contains("zyklisches") && error.contains("outer.bi") && error.contains("inner.bi")
    );
    t.write("inner.bi", "'$INCLUDE: 'missing.bi'\n");
    let error = loader.load(&main).unwrap_err();
    assert!(error.contains("inner.bi:1") && error.contains("missing.bi"));
    t.write("main.bas","PRINT \"$INCLUDE: 'missing.bi'\"\n' Dieser Kommentar erwähnt $INCLUDE: 'missing.bi'\nDATA $INCLUDE: 'missing.bi'\nEND\n");
    let input = loader.load(&main).unwrap();
    assert!(input.units[0]
        .segments
        .iter()
        .any(|s| s.text.starts_with("PRINT")));
    t.write("main.bas", "PRINT 1: '$INCLUDE: 'good.bi'\nEND\n");
    t.write("good.bi", "PRINT 2\n");
    let input = loader.load(&main).unwrap();
    let code = input.units[0]
        .segments
        .iter()
        .map(|s| s.text.as_str())
        .collect::<String>();
    assert!(code.contains("PRINT 1:") && code.contains("PRINT 2"));
    t.write("main.bas", "'$INCLUDE: 'unterminated\n");
    assert!(loader.load(&main).unwrap_err().contains("leeres $INCLUDE"));
}
#[test]
fn startup_metadata_roundtrip_and_invalid_contracts() {
    let t = Temp::new();
    t.write("a.bas", "END\n");
    t.write("b.bas", "");
    let p = t.write(
        "p.mak",
        "' keep this\nA.BAS\n\n' $STARTUP: \"b.bas\"\nb.bas\n",
    );
    let loader = SourceLoader::default();
    let input = loader.load(&p).unwrap();
    let output = input.manifest.text(&t.0.join("nested/new.mak")).unwrap();
    assert!(output.contains("../a.bas") || output.contains("../A.BAS"));
    assert!(output.contains("' keep this"));
    let roundtrip = Manifest::parse(&t.0.join("nested/new.mak"), &output, &loader).unwrap();
    assert_eq!(roundtrip.members().count(), 2);
    assert_eq!(
        identity(roundtrip.startup.as_ref().unwrap()).unwrap(),
        identity(&t.0.join("b.bas")).unwrap()
    );
    for text in [
        "' $STARTUP: \"a.bas\"\n' $STARTUP: \"a.bas\"\na.bas\n",
        "' $STARTUP: a.bas\na.bas\n",
        "' $STARTUP: \"b.bas\"\na.bas\n",
        "' $STARTUP: \"\"\na.bas\n",
        "' $STARTUP: \"missing.bas\"\na.bas\n",
    ] {
        assert!(Manifest::parse(&p, text, &loader).is_err(), "{text}");
    }
    assert!(Manifest::parse(&p, "a.bas\n", &loader)
        .unwrap()
        .startup
        .is_none());
    assert_eq!(
        relative_path(&t.0.join("nested"), &t.0.join("a.bas")).unwrap(),
        "../a.bas"
    );
}

#[test]
fn documented_mak_example_is_accepted_by_the_shared_loader() {
    let t = Temp::new();
    t.write("main.bas", "END\n");
    t.write("lib.bas", "");
    t.write("form.frm", "VERSION 1.00\nBegin Form F\nEnd\n");
    let docs = include_str!("../../../docs/dateiformate.md");
    let example = docs
        .split("```mak\n")
        .nth(1)
        .unwrap()
        .split("```")
        .next()
        .unwrap();
    let path = t.write("example.mak", example);
    let result = SourceLoader::default().load(&path).unwrap();
    assert_eq!(result.units.len(), 3);
    assert_eq!(result.forms.len(), 1);
    assert_eq!(
        result.manifest.startup.unwrap().file_name().unwrap(),
        "main.bas"
    );
}

#[test]
fn case_insensitive_parent_lookup_works_from_relative_base_without_changing_cwd() {
    let t = Temp::new();
    let sibling = t.write("Sibling.bas", "END\n");
    fs::create_dir(t.0.join("child")).unwrap();
    let base = PathBuf::from(
        relative_path(&std::env::current_dir().unwrap(), &t.0.join("child")).unwrap(),
    );
    let found = tb_vm::project_io::relative_case_insensitive(&base, "../SIBLING.BAS").unwrap();
    assert_eq!(identity(&found).unwrap(), identity(&sibling).unwrap());
}
