import os
os.chdir(r'C:\Users\decid\Documents\projects\spt-core\.worktrees\351-w0')

class F:
    def __init__(self, p):
        self.p = p
        raw = open(p, 'rb').read().decode('utf-8')
        self.crlf = '\r\n' in raw
        self.s = raw.replace('\r\n', '\n')
    def sub(self, a, b, n=1):
        c = self.s.count(a)
        assert c == n, (self.p, a[:80], c)
        self.s = self.s.replace(a, b)
    def save(self):
        out = self.s.replace('\n', '\r\n') if self.crlf else self.s
        open(self.p, 'wb').write(out.encode('utf-8'))

m = F('crates/spt/src/picker/model.rs')
m.sub('''/// A confirm-layer option. Resume/Fork/Instantiate/ChangeAdapter availability is''', '''/// A confirm-layer option. Resume/Fork/ChangeAdapter availability is''')
m.sub('''remote id (node-anchored identity, ADR-0003/0023). `Instantiate locally`
            // (pushed below on !is_local) stays the separate deliberate-copy verb.''', '''remote id (node-anchored identity, ADR-0003/0023). `Span to node`
            // (pushed below) is the deliberate way to put an instance here.''')
m.sub('''A remote offline endpoint gets Instantiate instead.''', '''A remote offline endpoint can be spanned here instead.''')
m.sub('''an online Attach/View, Fork/ChangeAdapter/Instantiate, the''', '''an online Attach/View, Fork/ChangeAdapter/Span, the''')
m.sub('''    /// (Resume / ChangeAdapter / Instantiate / Fork) return `None`''', '''    /// (Resume / ChangeAdapter / Span / Fork) return `None`''')
m.sub('''    /// path shared by Change-harness-adapter / Instantiate-locally (keep the id)
    /// and Fork (fresh id). All three converge''', '''    /// path for Fork (fresh id); `keep_id` keeps the existing id. Both converge''')
m.sub('''    // with ledger rows gets Resume; online does not; remote gets Instantiate.''', '''    // with ledger rows gets Resume; online does not; every row offers Span.''')
m.sub('''        assert!(!opts.contains(&ConfirmOption::Instantiate), "local ⇒ no Instantiate");''', '''        assert!(opts.contains(&ConfirmOption::Span), "local ⇒ Span to node");''')
m.sub('''        // remote: Instantiate present.''', '''        // remote: Span present.''')
m.sub('''        assert!(m3.confirm_options().contains(&ConfirmOption::Instantiate));''', '''        assert!(m3.confirm_options().contains(&ConfirmOption::Span));''')
m.sub('''co-gate b). `Instantiate` (the deliberate local-copy verb) survives.''', '''co-gate b). `Span to node` (the deliberate sibling verb) is offered.''')
m.sub('''        assert!(opts.contains(&ConfirmOption::Instantiate), "remote ⇒ Instantiate survives");''', '''        assert!(opts.contains(&ConfirmOption::Span), "remote ⇒ Span to node");''')
m.sub('''    // [unit->REQ-RUN-PICKER] Change-adapter / Instantiate re-enter create keeping
    // the id; Fork re-enters with a fresh id''', '''    // [unit->REQ-RUN-PICKER] re-entering create can keep the id; Fork re-enters
    // with a fresh id''')
m.sub('''        assert_eq!(m.id_buffer, "cold", "change-adapter/instantiate keep the id");''', '''        assert_eq!(m.id_buffer, "cold", "keep_id keeps the id");''')
m.sub('''    // [unit->REQ-PICKER-REMOTE-WAKE] confirm_terminal(Wake) yields''', '''    // [unit->REQ-PICKER-SPAN-TO-NODE] the span list: members of a subnet the id
    // is advertised in, minus every hosting node, minus this node when an
    // instance is already here; a non-member is never offered. Choosing a
    // remote node carries its raw hex, and this node carries an empty node.
    #[test]
    fn span_pick_lists_the_nodes_the_endpoint_can_span_to() {
        use std::collections::{BTreeMap, BTreeSet};
        let node = |hex: &str, subnets: &[&str], is_self: bool| SpanNode {
            hex: hex.into(),
            label: format!("L-{hex}"),
            subnets: subnets.iter().map(|s| s.to_string()).collect(),
            is_self,
        };
        let placement = |subnets: &[&str], hosts: &[&str]| SpanPlacement {
            subnets: subnets.iter().map(|s| s.to_string()).collect::<BTreeSet<_>>(),
            hosts: hosts.iter().map(|s| s.to_string()).collect::<BTreeSet<_>>(),
        };
        let nodes = vec![
            node("me", &["home"], true),
            node("bb", &["home"], false),
            node("cc", &["home", "work"], false),
            node("dd", &["other"], false),
        ];

        // A remote row hosted on bb: this node, and cc, may take it; bb hosts
        // it and dd is in no subnet it is advertised in.
        let remote = ep("ling", "subnet:n", EpStatus::Online, false);
        let mut m = PickerModel::new("p".into(), vec![], vec![remote], None, None, vec![]);
        m.span_nodes = nodes.clone();
        m.span_index = BTreeMap::from([("ling".to_string(), placement(&["home"], &["bb"]))]);
        m.category = Category::Subnet;
        m.enter_pick();
        let hexes: Vec<&str> = m.span_choices().iter().map(|n| n.hex.as_str()).collect();
        assert_eq!(hexes, vec!["me", "cc"]);
        m.enter_span_pick();
        assert_eq!(m.screen, Screen::SpanPick);
        assert_eq!(
            m.span_outcome(),
            Some(Outcome::Span { id: "ling".into(), node: String::new() }),
            "this node spans with no --node"
        );
        m.move_span(1);
        assert_eq!(
            m.span_outcome(),
            Some(Outcome::Span { id: "ling".into(), node: "cc".into() })
        );
        assert!(!m.back());
        assert_eq!(m.screen, Screen::Confirm);

        // A local row: an instance is here, so this node is not offered.
        let local = ep("ling", "g", EpStatus::Online, true);
        let mut m = PickerModel::new("p".into(), vec![], vec![local], None, None, vec![]);
        m.span_nodes = nodes;
        m.span_index = BTreeMap::from([("ling".to_string(), placement(&["home"], &["me"]))]);
        m.category = Category::Local;
        m.enter_pick();
        let hexes: Vec<&str> = m.span_choices().iter().map(|n| n.hex.as_str()).collect();
        assert_eq!(hexes, vec!["bb", "cc"]);
    }

    // [unit->REQ-PICKER-REMOTE-WAKE] confirm_terminal(Wake) yields''')
m.save()

d = F('crates/spt/src/picker/mod.rs')
d.sub('''                // Instantiate keeps the id through the create flow; Fork starts fresh.
                ConfirmOption::Instantiate => {
                    model.reenter_create(true);
                    None
                }''', '''                // Span to node: pick the node, then run the span verb.
                // [impl->REQ-PICKER-SPAN-TO-NODE]
                ConfirmOption::Span => {
                    model.enter_span_pick();
                    None
                }''')
d.sub('''        Screen::ChooseProject => match code {''', '''        // [impl->REQ-PICKER-SPAN-TO-NODE]
        Screen::SpanPick => match code {
            KeyCode::Up => model.move_span(-1),
            KeyCode::Down => model.move_span(1),
            KeyCode::Enter => return model.span_outcome(),
            _ => {}
        },
        Screen::ChooseProject => match code {''')
d.sub('''                | Outcome::Wake { .. }
                | Outcome::Cancelled) => return Ok(leaving),''', '''                | Outcome::Wake { .. }
                | Outcome::Span { .. }
                | Outcome::Cancelled) => return Ok(leaving),''')
d.sub('''        Outcome::Wake { id, node } => crate::cli::cmd_endpoint_wake_remote(&id, &node),''', '''        Outcome::Wake { id, node } => crate::cli::cmd_endpoint_wake_remote(&id, &node),
        // The same verb `spt endpoint span` runs (REQ-PICKER-SPAN-TO-NODE).
        Outcome::Span { id, node } => {
            crate::cli::cmd_endpoint_span(&id, (!node.is_empty()).then_some(node.as_str()), None)
        }''')
d.sub('''    model.adapter_versions = data::adapter_versions();
''', '''    model.adapter_versions = data::adapter_versions();
    // [impl->REQ-PICKER-SPAN-TO-NODE]
    model.span_nodes = data::span_nodes();
    model.span_index = data::span_index();
''')
d.save()

c = F('crates/spt/src/cli.rs')
c.sub('''fn cmd_endpoint_span(id: &str''', '''pub(crate) fn cmd_endpoint_span(id: &str''')
c.save()
print('ok')
