fusible 0.20.0

Electronic linter: a fuse that blows to block firmware code generation
use std::path::{Path, PathBuf};

use super::live::{compare_board, sync_positions};

pub(super) fn scratch(tag: &str) -> PathBuf {
    let dir = std::env::temp_dir().join(format!("fusible_live_{tag}_{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();
    dir
}

fn component(name: &str, x: f64) -> String {
    format!(
        r#"{{ "type": "source_component", "source_component_id": "sc_{name}", "name": "{name}" }},
        {{ "type": "pcb_component", "source_component_id": "sc_{name}", "center": {{ "x": {x}, "y": 0 }},
          "rotation": 0, "layer": "top" }},
        {{ "type": "source_port", "source_component_id": "sc_{name}", "pin_number": 1,
          "subcircuit_connectivity_map_key": "net_{name}" }}"#
    )
}

pub(super) fn circuit(names: &[&str]) -> String {
    let parts: Vec<String> = names
        .iter()
        .enumerate()
        .map(|(i, n)| component(n, i as f64 * 3.0))
        .collect();
    format!("[{}]", parts.join(","))
}

fn footprint(name: &str, x: f64, net: &str) -> String {
    format!(
        r#"(footprint "R" (layer "F.Cu") (at {x} 40) (property "Reference" "{name}")
            (pad "1" smd rect (net 1 "{net}")))"#
    )
}

pub(super) fn write_board(dir: &Path, parts: &[(&str, f64, &str)]) -> PathBuf {
    let body: Vec<String> = parts
        .iter()
        .map(|(n, x, net)| footprint(n, *x, net))
        .collect();
    let path = dir.join("board.kicad_pcb");
    std::fs::write(&path, format!("(kicad_pcb {})", body.join(" "))).unwrap();
    path
}

pub(super) fn write_tsx(dir: &Path, parts: &[&str]) -> PathBuf {
    let tags: Vec<String> = parts
        .iter()
        .enumerate()
        .map(|(i, n)| format!("<resistor name=\"{n}\" pcbX={{{}}} pcbY={{0}} />", i * 3))
        .collect();
    let path = dir.join("index.tsx");
    std::fs::write(&path, format!("<board>\n{}\n</board>\n", tags.join("\n"))).unwrap();
    path
}

#[test]
fn moved_footprint_is_written_to_the_tsx_and_the_previous_file_is_backed_up() {
    let dir = scratch("moved");
    let pcb = write_board(
        &dir,
        &[("R1", 100.0, "A"), ("R2", 103.0, "B"), ("R3", 130.0, "C")],
    );
    let tsx = write_tsx(&dir, &["R1", "R2", "R3"]);
    let before = std::fs::read_to_string(&tsx).unwrap();
    let comparison = compare_board(&pcb, &circuit(&["R1", "R2", "R3"])).unwrap();
    let backups = dir.join(".fusible/backup");
    let moved = sync_positions(&comparison, &dir, &backups, "main").unwrap();
    assert_eq!(moved, ["R3"]);
    let after = std::fs::read_to_string(&tsx).unwrap();
    assert_eq!(
        after,
        before.replace("name=\"R3\" pcbX={6}", "name=\"R3\" pcbX={30}")
    );
    let copies: Vec<_> = std::fs::read_dir(&backups).unwrap().flatten().collect();
    assert_eq!(copies.len(), 1);
    let copy = copies[0].path();
    assert!(copy.extension().is_some_and(|e| e == "tsx"));
    assert_eq!(std::fs::read_to_string(copy).unwrap(), before);
    assert!(comparison.differences.is_empty());
}

#[test]
fn invalid_circuit_json_is_an_error_not_an_empty_circuit() {
    let dir = scratch("invalid");
    let pcb = write_board(&dir, &[("R1", 100.0, "A")]);
    let truncated = r#"[{"type": "source_component", "name": "R1""#;
    let error = compare_board(&pcb, truncated).err().unwrap();
    assert!(error.contains("invalid Circuit JSON"), "{error}");
}