use std::{
    fs,
    path::PathBuf,
    sync::atomic::{AtomicUsize, Ordering},
};
use tb_ide::documents::{Decision, Destination, Project, SavePlan, StartupRemoval};
use tb_vm::project_io::{Content, SourceLoader};
struct Temp(PathBuf);
impl Temp {
    fn new() -> Self {
        static N: AtomicUsize = AtomicUsize::new(0);
        let p = std::env::temp_dir().join(format!(
            "tb-documents-{}-{}",
            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
    }
    fn plan(&self, name: &str) -> SavePlan {
        SavePlan {
            project: Some(Destination::new(self.0.join(name))),
            ..SavePlan::default()
        }
    }
}
impl Drop for Temp {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.0);
    }
}

#[test]
fn project_create_save_reopen_preserves_forms_members_startup_and_cli_sources() {
    let t = Temp::new();
    let mut p = Project::new(&t.0).unwrap();
    let a = p.new_module("Main").unwrap();
    let b = p.new_module("Lib").unwrap();
    let form = p.new_form("Form1").unwrap();
    p.replace_text(a, 0..0, "CALL Answer\nEND\n").unwrap();
    p.replace_text(b, 0..0, "SUB Answer\nPRINT 7\nEND SUB\n")
        .unwrap();
    p.edit_form(form,|f| {let parsed=tb_ui::frm::read_text("f.frm","VERSION 1.00\nBegin Form Form1\n Begin CommandButton Button1\n  Index = 0\n End\n Begin CommandButton Button1\n  Index = 2\n End\nEnd\n\nSUB Button1_Click(Index AS INTEGER)\nEND SUB\n")?;*f=parsed;Ok(())}).unwrap();
    p.set_startup(Some(a)).unwrap();
    let mut plan = t.plan("p.mak");
    for (id, name) in [(a, "Main.bas"), (b, "Lib.bas"), (form, "Form1.frm")] {
        plan.files.insert(id, Destination::new(t.0.join(name)));
    }
    p.save_project(&plan).unwrap();
    assert!(!p.is_dirty());
    let restored = Project::open(&t.0.join("p.mak"), vec![]).unwrap();
    assert_eq!(restored.members().len(), 3);
    assert_eq!(
        restored
            .document(restored.startup().unwrap())
            .unwrap()
            .path()
            .unwrap()
            .file_name()
            .unwrap(),
        "Main.bas"
    );
    let input = restored.sources().unwrap();
    assert_eq!(input.forms[0].root.children.len(), 2);
    assert!(input.forms[0].code.contains("Button1_Click"));
    assert!(fs::read_to_string(t.0.join("Form1.frm"))
        .unwrap()
        .lines()
        .any(|line| line.split_whitespace().collect::<Vec<_>>() == ["Index", "=", "0"]));
    let disk = SourceLoader::default().load(&t.0.join("p.mak")).unwrap();
    assert_eq!(disk.forms, input.forms);
    let mut catalog = tb_frontend::forms::FormCatalog::default();
    for f in &disk.forms {
        catalog.append(&f.catalog());
    }
    tb_vm::compile_project("P", &disk.units, &catalog, &disk.forms).unwrap();
}

#[cfg(unix)]
#[test]
fn unrepresentable_save_paths_fail_before_writing_or_changing_documents() {
    use std::{ffi::OsString, os::unix::ffi::OsStringExt};
    let t = Temp::new();
    let mut p = Project::new(&t.0).unwrap();
    let id = p.new_module("Main").unwrap();
    p.replace_text(id, 0..0, "PRINT 1\n").unwrap();
    for name in [
        OsString::from_vec(b"bad\xff.bas".to_vec()),
        "bad\n.bas".into(),
    ] {
        let target = t.0.join(name);
        assert!(p.save_file(id, Some(&Destination::new(&target))).is_err());
        assert!(!target.exists());
        assert!(p.document(id).unwrap().path().is_none());
        assert!(p.document(id).unwrap().is_dirty());
        assert_eq!(p.document(id).unwrap().code(), "PRINT 1\n");
    }
    let manifest = tb_vm::project_io::Manifest {
        lines: vec![tb_vm::project_io::ProjectLine::File(
            t.0.join(OsString::from_vec(b"bad\xff.bas".to_vec())),
        )],
        startup: None,
    };
    assert!(manifest.text(&t.0.join("p.mak")).is_err());
    assert_eq!(fs::read_dir(&t.0).unwrap().count(), 0);
}
#[test]
fn shared_views_text_import_and_undo_are_document_transactions() {
    let t = Temp::new();
    let mut p = Project::new(&t.0).unwrap();
    let id = p.new_module("Main").unwrap();
    p.replace_text(id, 0..0, "äbcdef\n").unwrap();
    let a = p.open_view(id).unwrap();
    let b = p.open_view(id).unwrap();
    p.view_mut(a).unwrap().cursor = 2;
    p.view_mut(b).unwrap().cursor = 7;
    p.view_mut(b).unwrap().scroll_line = 9;
    let import = t.write("text.txt", "HELLO");
    p.load_text(id, 2, &import).unwrap();
    assert_eq!(p.view_code(a).unwrap(), "äHELLObcdef\n");
    assert_eq!(p.view_code(a).unwrap(), p.view_code(b).unwrap());
    assert_eq!(p.view(b).unwrap().cursor, 12);
    assert_eq!(p.view(a).unwrap().cursor, 2);
    assert!(p.undo(id).unwrap());
    assert_eq!(p.document(id).unwrap().code(), "äbcdef\n");
    assert_eq!(p.view(b).unwrap().cursor, 7);
    assert_eq!(p.view(b).unwrap().scroll_line, 9);
    p.replace_text(id, 2..7, "").unwrap();
    assert_eq!(p.view(b).unwrap().cursor, 2);
    p.undo(id).unwrap();
    p.close_view(a).unwrap();
    assert!(p.document(id).is_ok());
    assert_eq!(p.view_code(b).unwrap(), "äbcdef\n");
    let rev = p.document(id).unwrap().revision();
    assert!(p.replace_text(id, 1..2, "bad").is_err());
    assert_eq!(p.document(id).unwrap().revision(), rev);
    p.save_text(id, Some(0..2), &Destination::new(t.0.join("selection.txt")))
        .unwrap();
    assert_eq!(fs::read_to_string(t.0.join("selection.txt")).unwrap(), "ä");
}
#[test]
fn edited_include_wins_in_both_modules_without_writing_the_expansion() {
    let t = Temp::new();
    let path = t.write("p.mak", "a.bas\nb.bas\n");
    t.write("a.bas", "'$INCLUDE: 'shared.bi'\nEND\n");
    t.write("b.bas", "'$INCLUDE: 'shared.bi'\n");
    let file = t.write("shared.bi", "CONST N=1\n");
    let mut p = Project::open(&path, vec![]).unwrap();
    let id = p.open_document(&file).unwrap();
    assert_eq!(p.open_document(&file).unwrap(), id);
    p.replace_text(id, 8..9, "9").unwrap();
    let input = p.sources().unwrap();
    for u in input.units {
        assert!(u.segments.iter().any(|s| s.text == "CONST N=9\n"));
    }
    assert_eq!(fs::read_to_string(&file).unwrap(), "CONST N=1\n");
    p.save_file(id, None).unwrap();
    assert_eq!(fs::read_to_string(&file).unwrap(), "CONST N=9\n");
    assert_eq!(
        fs::read_to_string(t.0.join("a.bas")).unwrap(),
        "'$INCLUDE: 'shared.bi'\nEND\n"
    );
}
#[test]
fn failed_open_add_and_cancel_do_not_replace_existing_documents() {
    let t = Temp::new();
    let mut p = Project::new(&t.0).unwrap();
    let id = p.new_module("Current").unwrap();
    p.replace_text(id, 0..0, "PRINT 1\n").unwrap();
    let cyclic = t.write("cyclic.bas", "'$INCLUDE: 'cyclic.bas'\n");
    let missing = t.write("bad.mak", "missing.bas\n");
    for path in [cyclic.clone(), missing] {
        assert!(p.open_project(&path, Decision::Discard).is_err());
        assert_eq!(p.document(id).unwrap().code(), "PRINT 1\n");
    }
    assert!(p.add_file(&cyclic).is_err());
    assert_eq!(p.members(), vec![id]);
    assert!(!p.new_project(Decision::Cancel).unwrap());
    assert!(!p.prepare_close(Decision::Cancel).unwrap());
    assert!(p.document(id).unwrap().is_dirty());
    let valid = t.write("valid.bas", "END\n");
    assert!(!p.open_project(&valid, Decision::Cancel).unwrap());
    assert!(p.open_project(&valid, Decision::Discard).unwrap());
    assert!(p.document(id).is_err());
}
#[test]
fn startup_removal_requires_explicit_replacement_and_never_deletes_a_file() {
    let t = Temp::new();
    let path = t.write("p.mak", "' header\na.bas\nb.bas\n");
    let a_path = t.write("a.bas", "END\n");
    t.write("b.bas", "");
    let mut p = Project::open(&path, vec![]).unwrap();
    let ids = p.members();
    p.set_startup(Some(ids[0])).unwrap();
    assert!(p.remove_file(ids[0], None).is_err());
    assert!(!p.remove_file(ids[0], Some(StartupRemoval::Cancel)).unwrap());
    assert_eq!(p.members(), ids);
    assert!(p
        .remove_file(ids[0], Some(StartupRemoval::Replace(ids[0])))
        .is_err());
    assert_eq!(p.startup(), Some(ids[0]));
    p.remove_file(ids[0], Some(StartupRemoval::Replace(ids[1])))
        .unwrap();
    assert_eq!(p.startup(), Some(ids[1]));
    assert!(a_path.exists());
    assert!(p.document(ids[0]).is_ok());
    p.save_project(&SavePlan::default()).unwrap();
    let saved = fs::read_to_string(path).unwrap();
    assert!(saved.contains("' header"));
    assert!(!saved.lines().any(|l| l == "a.bas"));
    p.remove_file(ids[1], Some(StartupRemoval::Default))
        .unwrap();
    assert_eq!(p.startup(), None);
}
#[test]
fn external_changes_and_save_as_collisions_need_explicit_decisions() {
    let t = Temp::new();
    let file = t.write("main.bas", "old\n");
    let mut p = Project::open(&file, vec![]).unwrap();
    let id = p.members()[0];
    p.replace_text(id, 0..3, "new").unwrap();
    fs::write(&file, "external\n").unwrap();
    assert!(p
        .save_file(id, None)
        .unwrap_err()
        .to_string()
        .contains("Überschreibentscheidung"));
    assert_eq!(fs::read_to_string(&file).unwrap(), "external\n");
    assert!(p.document(id).unwrap().is_dirty());
    p.save_file(
        id,
        Some(&Destination {
            path: file.clone(),
            overwrite: true,
        }),
    )
    .unwrap();
    assert_eq!(fs::read_to_string(&file).unwrap(), "new\n");
    let other = t.write("other.bas", "keep\n");
    assert!(p.save_file(id, Some(&Destination::new(&other))).is_err());
    assert_eq!(p.document(id).unwrap().path(), Some(file.as_path()));
    p.save_file(
        id,
        Some(&Destination {
            path: other.clone(),
            overwrite: true,
        }),
    )
    .unwrap();
    assert_eq!(fs::read_to_string(&other).unwrap(), "new\n");
    assert_eq!(p.document(id).unwrap().path(), Some(other.as_path()));
    assert!(p
        .save_text(
            id,
            None,
            &Destination {
                path: other,
                overwrite: true
            }
        )
        .is_err());
}
#[test]
fn partial_save_preserves_unsaved_data_and_does_not_finish_close() {
    let t = Temp::new();
    let path = t.write("p.mak", "a.bas\nb.bas\n");
    let a = t.write("a.bas", "old A\n");
    let b = t.write("b.bas", "old B\n");
    let mut p = Project::open(&path, vec![]).unwrap();
    let ids = p.members();
    for id in &ids {
        p.replace_text(*id, 0..3, "new").unwrap();
    }
    let mut perms = fs::metadata(&b).unwrap().permissions();
    perms.set_readonly(true);
    fs::set_permissions(&b, perms).unwrap();
    let result = p.prepare_close(Decision::Save(&SavePlan::default()));
    assert!(result.is_err());
    assert!(!p.document(ids[0]).unwrap().is_dirty());
    assert!(p.document(ids[1]).unwrap().is_dirty());
    assert!(p.is_dirty());
    assert_eq!(p.path(), Some(path.as_path()));
    assert_eq!(fs::read_to_string(&a).unwrap(), "new A\n");
    assert_eq!(fs::read_to_string(&b).unwrap(), "old B\n");
    assert_eq!(fs::read_to_string(&path).unwrap(), "a.bas\nb.bas\n");
    // Die ursprünglichen Schreibrechte aus einer normalen Datei wiederherstellen.
    fs::set_permissions(&b, fs::metadata(&a).unwrap().permissions()).unwrap();
    assert!(p
        .prepare_close(Decision::Save(&SavePlan::default()))
        .unwrap());
    assert!(!p.is_dirty());
    assert!(fs::read_dir(&t.0).unwrap().all(|e| !e
        .unwrap()
        .file_name()
        .to_string_lossy()
        .starts_with(".tb-save-")));
}
#[test]
fn project_save_as_rebases_members_and_keeps_include_targets() {
    let t = Temp::new();
    let path = t.write("old/p.mak", "main.bas\n' retained\n");
    t.write("old/main.bas", "'$INCLUDE: 'inner.bi'\nEND\n");
    t.write("old/inner.bi", "PRINT 3\n");
    fs::create_dir(t.0.join("new")).unwrap();
    let mut p = Project::open(&path, vec![]).unwrap();
    let before = p.sources().unwrap();
    let target = t.0.join("new/q.mak");
    p.save_project(&t.plan("new/q.mak")).unwrap();
    assert_eq!(p.path(), Some(target.as_path()));
    let reopened = Project::open(&target, vec![]).unwrap();
    assert_eq!(
        before.units[0]
            .segments
            .iter()
            .map(|s| &s.text)
            .collect::<Vec<_>>(),
        reopened.sources().unwrap().units[0]
            .segments
            .iter()
            .map(|s| &s.text)
            .collect::<Vec<_>>()
    );
    assert!(fs::read_to_string(&target)
        .unwrap()
        .contains("../old/main.bas"));
    let bad = t.plan("missing/q.mak");
    assert!(p.save_project(&bad).is_err());
    assert_eq!(p.path(), Some(target.as_path()));
}
#[test]
fn binary_import_only_saves_to_explicit_text_target_and_preserves_original_forever() {
    let t = Temp::new();
    let bytes: Vec<u8> = include_str!("../../tb-ui/tests/data/new.frm.hex")
        .split_whitespace()
        .map(|s| u8::from_str_radix(s, 16).unwrap())
        .collect();
    let binary = t.0.join("source.frm");
    fs::write(&binary, &bytes).unwrap();
    let mut p = Project::open(&binary, vec![]).unwrap();
    let id = p.members()[0];
    p.replace_text(id, 0..0, "SUB Form_Load\nEND SUB\n")
        .unwrap();
    assert!(p
        .save_file(id, None)
        .unwrap_err()
        .to_string()
        .contains("binäre FRM"));
    let text = t.0.join("converted.frm");
    p.save_file(id, Some(&Destination::new(&text))).unwrap();
    assert!(fs::read_to_string(&text).unwrap().starts_with("VERSION"));
    assert_eq!(fs::read(&binary).unwrap(), bytes);
    assert!(p
        .save_file(
            id,
            Some(&Destination {
                path: binary.clone(),
                overwrite: true
            })
        )
        .is_err());
    let original = p.document(id).unwrap().content().clone();
    p.edit_form(id, |f| {
        f.root.name = "Edited".into();
        f.code.push_str("' changed\n");
        Ok(())
    })
    .unwrap();
    assert!(p.undo(id).unwrap());
    assert_eq!(p.document(id).unwrap().content(), &original);
    let mut plan = t.plan("converted.mak");
    p.save_project(&plan).unwrap();
    plan.project = None;
    let restored = Project::open(&t.0.join("converted.mak"), vec![]).unwrap();
    assert!(matches!(
        restored.document(restored.members()[0]).unwrap().content(),
        Content::Form(_)
    ));
}
#[test]
fn save_plan_prevents_same_destination_and_project_overwrite() {
    let t = Temp::new();
    let mut p = Project::new(&t.0).unwrap();
    let a = p.new_module("A").unwrap();
    let b = p.new_module("B").unwrap();
    let mut plan = t.plan("p.mak");
    let target = t.0.join("same.bas");
    plan.files.insert(a, Destination::new(&target));
    plan.files.insert(b, Destination::new(&target));
    assert!(p.save_project(&plan).is_err());
    assert!(!target.exists());
    assert!(p.document(a).unwrap().is_dirty());
    assert!(p
        .save_file(a, Some(&Destination::new(t.0.join("bad.frm"))))
        .is_err());
    assert!(p.new_module("../bad").is_err());
}

#[test]
fn module_save_as_rebases_includes_and_failed_save_does_not_change_identity_or_text() {
    let t = Temp::new();
    let path = t.write("old/p.mak", "main.bas\n");
    let original = "'$INCLUDE: 'common.bi' ' keep comment\nEND\n";
    t.write("old/main.bas", original);
    t.write("old/common.bi", "PRINT 42\n");
    fs::create_dir(t.0.join("new")).unwrap();
    let mut p = Project::open(&path, vec![]).unwrap();
    let id = p.members()[0];
    let old = p.document(id).unwrap().source_path().to_path_buf();
    assert!(p
        .save_file(id, Some(&Destination::new(t.0.join("missing/main.bas"))))
        .is_err());
    assert_eq!(p.document(id).unwrap().source_path(), old);
    assert_eq!(p.document(id).unwrap().code(), original);
    let view = p.open_view(id).unwrap();
    p.save_file(id, Some(&Destination::new(t.0.join("new/main.bas"))))
        .unwrap();
    assert!(p
        .document(id)
        .unwrap()
        .code()
        .contains("../old/common.bi' ' keep comment"));
    assert_eq!(p.view(view).unwrap().document(), id);
    p.save_project(&SavePlan::default()).unwrap();
    let restored = Project::open(&path, vec![]).unwrap();
    assert!(restored.sources().unwrap().units[0]
        .segments
        .iter()
        .any(|s| s.text == "PRINT 42\n"));
    assert_eq!(fs::read_to_string(old).unwrap(), original);
}

#[test]
fn aliases_share_documents_and_form_code_offsets_match_saved_text() {
    let t = Temp::new();
    let file = t.write("Mixed.bas", "PRINT 1\n");
    let mut p = Project::new(&t.0).unwrap();
    let a = p.add_file(&file).unwrap();
    assert_eq!(p.open_document(&t.0.join("MIXED.BAS")).unwrap(), a);
    #[cfg(unix)]
    {
        std::os::unix::fs::symlink(&file, t.0.join("alias.bas")).unwrap();
        assert_eq!(p.open_document(&t.0.join("alias.bas")).unwrap(), a);
    }
    let id = p.new_form("F").unwrap();
    p.replace_text(id, 0..0, "SUB Form_Load\nERROR 6\nEND SUB\n")
        .unwrap();
    let target = t.0.join("F.frm");
    p.save_file(id, Some(&Destination::new(&target))).unwrap();
    // Der gemeinsame Speicherhook ergänzt DECLAREs; Speicherstand und Disk müssen dieselben physischen Zeilen liefern.
    let before = p.sources().unwrap();
    let disk = SourceLoader::default().load(&target).unwrap();
    assert_eq!(
        before.units[1]
            .segments
            .iter()
            .map(|s| (&s.text, s.first_line))
            .collect::<Vec<_>>(),
        disk.units[0]
            .segments
            .iter()
            .map(|s| (&s.text, s.first_line))
            .collect::<Vec<_>>()
    );
}

#[test]
fn cursor_after_shortening_uses_original_position_and_open_after_save_uses_fresh_disk() {
    let t = Temp::new();
    let path = t.write("p.mak", "main.bas\n");
    t.write("main.bas", "abcdefgh\n");
    let mut p = Project::open(&path, vec![]).unwrap();
    let id = p.members()[0];
    let v = p.open_view(id).unwrap();
    p.view_mut(v).unwrap().cursor = 9;
    p.replace_text(id, 1..2, "").unwrap();
    assert_eq!(p.view(v).unwrap().cursor, 8);
    p.undo(id).unwrap();
    assert_eq!(p.view(v).unwrap().cursor, 9);
    p.replace_text(id, 0..8, "PRINT 7").unwrap();
    assert!(p
        .open_project(&path, Decision::Save(&SavePlan::default()))
        .unwrap());
    assert_eq!(p.document(p.members()[0]).unwrap().code(), "PRINT 7\n");
}
