#!/usr/bin/env nu
# Tests for workspace/state.nu — state read/write/transition/decision functions.
# Each test creates an isolated temp workspace and cleans up on exit.

use std assert
use ../workspace/state.nu *

# ─── Helpers ─────────────────────────────────────────────────────────────────

def mk-tmp-workspace []: nothing -> string {
    let p = ($"/tmp/prov_state_test_(random chars --length 8)")
    mkdir $p
    $p
}

def with-tmp [body: closure]: nothing -> nothing {
    let ws = (mk-tmp-workspace)
    do $body $ws
    rm -rf $ws
}

# ─── state-read ──────────────────────────────────────────────────────────────

export def test_state_read_missing_file_returns_default [] {
    with-tmp {|ws|
        let st = (state-read $ws)
        assert ($st.servers | is-empty)
        assert equal $st.schema_version "2.0"
    }
    print "✓ state-read: missing file returns all-pending default"
}

# ─── state-write / roundtrip ─────────────────────────────────────────────────

export def test_state_write_read_roundtrip [] {
    with-tmp {|ws|
        let initial = {
            workspace:      "test",
            cluster:        "sgoyol",
            schema_version: "2.0",
            servers: {
                "sgoyol-0": {
                    provider_id:    "99",
                    provider_state: "running",
                    last_sync:      "2026-04-11T10:00:00Z",
                    taskservs: {},
                }
            }
        }
        state-write $ws $initial
        let back = (state-read $ws)
        assert equal $back.cluster "sgoyol"
        assert equal ($back.servers."sgoyol-0".provider_id) "99"
        assert equal ($back.servers."sgoyol-0".provider_state) "running"
    }
    print "✓ state-write/read: roundtrip preserves all fields"
}

export def test_state_write_is_atomic [] {
    with-tmp {|ws|
        let st = { workspace: "test", cluster: "c", schema_version: "2.0", servers: {} }
        state-write $ws $st
        # tmp file must not remain after write
        assert not (($ws | path join ".provisioning-state.ncl.tmp") | path exists)
        assert (state-path $ws | path exists)
    }
    print "✓ state-write: no .tmp file left after atomic write"
}

# ─── state-node-get ──────────────────────────────────────────────────────────

export def test_state_node_get_unknown_returns_pending [] {
    with-tmp {|ws|
        let node = (state-node-get $ws "sgoyol-0" "etcd")
        assert equal $node.state "pending"
        assert equal $node.blocker ""
    }
    print "✓ state-node-get: unknown node returns pending default"
}

# ─── state-node-start ────────────────────────────────────────────────────────

export def test_state_node_start_transitions_to_running [] {
    with-tmp {|ws|
        state-node-start $ws "sgoyol-0" "etcd" --actor "jesus" --source "cli" --operation "create"
        let node = (state-node-get $ws "sgoyol-0" "etcd")
        assert equal $node.state "running"
        assert equal $node.actor.identity "jesus"
        assert equal $node.actor.source "cli"
        assert ($node.started_at | is-not-empty)
        assert equal ($node.log | length) 1
        assert equal ($node.log | first | get event) "started"
    }
    print "✓ state-node-start: pending → running with actor + log entry"
}

# ─── state-node-finish ───────────────────────────────────────────────────────

export def test_state_node_finish_success [] {
    with-tmp {|ws|
        state-node-start $ws "sgoyol-0" "etcd" --actor "system" --source "orchestrator"
        state-node-finish $ws "sgoyol-0" "etcd" --success
        let node = (state-node-get $ws "sgoyol-0" "etcd")
        assert equal $node.state "completed"
        assert ($node.ended_at | is-not-empty)
        assert equal ($node.log | length) 2
        assert equal ($node.log | last | get event) "completed"
    }
    print "✓ state-node-finish: running → completed with ended_at + log entry"
}

export def test_state_node_finish_failure [] {
    with-tmp {|ws|
        state-node-start $ws "sgoyol-0" "containerd" --actor "system" --source "orchestrator"
        state-node-finish $ws "sgoyol-0" "containerd"
        let node = (state-node-get $ws "sgoyol-0" "containerd")
        assert equal $node.state "failed"
        assert equal ($node.log | last | get event) "failed"
    }
    print "✓ state-node-finish: running → failed with log entry"
}

# ─── state-node-block ────────────────────────────────────────────────────────

export def test_state_node_block [] {
    with-tmp {|ws|
        state-node-block $ws "sgoyol-0" "kubernetes" "containerd"
        let node = (state-node-get $ws "sgoyol-0" "kubernetes")
        assert equal $node.state "blocked"
        assert equal $node.blocker "containerd"
        assert equal ($node.log | last | get event) "blocked-by:containerd"
    }
    print "✓ state-node-block: → blocked with blocker field + log entry"
}

# ─── state-node-reset ────────────────────────────────────────────────────────

export def test_state_node_reset [] {
    with-tmp {|ws|
        state-node-start $ws "sgoyol-0" "cilium" --actor "jesus" --source "cli"
        state-node-finish $ws "sgoyol-0" "cilium" --success
        state-node-reset $ws "sgoyol-0" "cilium" --source "cli" --actor "jesus"
        let node = (state-node-get $ws "sgoyol-0" "cilium")
        assert equal $node.state "pending"
        assert equal $node.blocker ""
        assert equal $node.started_at ""
        assert equal $node.ended_at ""
        assert equal ($node.log | last | get event) "reset"
    }
    print "✓ state-node-reset: completed → pending, clears timestamps + blocker"
}

# ─── state-node-decision ─────────────────────────────────────────────────────

export def test_state_node_decision_completed_is_skip [] {
    with-tmp {|ws|
        state-node-start $ws "sgoyol-0" "etcd" --actor "s" --source "orchestrator"
        state-node-finish $ws "sgoyol-0" "etcd" --success
        assert equal (state-node-decision $ws "sgoyol-0" "etcd") "skip"
    }
    print "✓ state-node-decision: completed → skip"
}

export def test_state_node_decision_failed_is_rerun [] {
    with-tmp {|ws|
        state-node-start $ws "sgoyol-0" "etcd" --actor "s" --source "orchestrator"
        state-node-finish $ws "sgoyol-0" "etcd"
        assert equal (state-node-decision $ws "sgoyol-0" "etcd") "rerun"
    }
    print "✓ state-node-decision: failed → rerun"
}

export def test_state_node_decision_pending_is_run [] {
    with-tmp {|ws|
        assert equal (state-node-decision $ws "sgoyol-0" "etcd") "run"
    }
    print "✓ state-node-decision: pending → run"
}

export def test_state_node_decision_blocked_is_blocked [] {
    with-tmp {|ws|
        state-node-block $ws "sgoyol-0" "kubernetes" "containerd"
        assert equal (state-node-decision $ws "sgoyol-0" "kubernetes") "blocked"
    }
    print "✓ state-node-decision: blocked → blocked"
}

# ─── state-dag-check-deps ────────────────────────────────────────────────────

export def test_dag_check_deps_empty_is_ready [] {
    with-tmp {|ws|
        let r = (state-dag-check-deps $ws "sgoyol-0" [])
        assert $r.ready
        assert equal $r.blocker ""
    }
    print "✓ state-dag-check-deps: empty deps → ready"
}

export def test_dag_check_deps_all_completed_is_ready [] {
    with-tmp {|ws|
        state-node-start $ws "sgoyol-0" "etcd" --actor "s" --source "orchestrator"
        state-node-finish $ws "sgoyol-0" "etcd" --success
        state-node-start $ws "sgoyol-0" "containerd" --actor "s" --source "orchestrator"
        state-node-finish $ws "sgoyol-0" "containerd" --success
        let r = (state-dag-check-deps $ws "sgoyol-0" ["etcd" "containerd"])
        assert $r.ready
        assert equal $r.blocker ""
    }
    print "✓ state-dag-check-deps: all completed → ready"
}

export def test_dag_check_deps_failed_dep_blocks [] {
    with-tmp {|ws|
        state-node-start $ws "sgoyol-0" "containerd" --actor "s" --source "orchestrator"
        state-node-finish $ws "sgoyol-0" "containerd"
        let r = (state-dag-check-deps $ws "sgoyol-0" ["containerd"])
        assert not $r.ready
        assert equal $r.blocker "containerd"
    }
    print "✓ state-dag-check-deps: failed dep → not ready, returns blocker"
}

export def test_dag_check_deps_pending_dep_blocks [] {
    with-tmp {|ws|
        # etcd never started → pending
        let r = (state-dag-check-deps $ws "sgoyol-0" ["etcd"])
        assert not $r.ready
        assert equal $r.blocker "etcd"
    }
    print "✓ state-dag-check-deps: pending dep → not ready, returns blocker"
}

# ─── state-node-decision-with-deps ───────────────────────────────────────────

export def test_decision_with_deps_skips_when_completed [] {
    with-tmp {|ws|
        state-node-start $ws "sgoyol-0" "cilium" --actor "s" --source "orchestrator"
        state-node-finish $ws "sgoyol-0" "cilium" --success
        # Even with pending deps, skip wins — already done
        let d = (state-node-decision-with-deps $ws "sgoyol-0" "cilium" ["kubernetes"])
        assert equal $d "skip"
    }
    print "✓ state-node-decision-with-deps: own completed → skip regardless of deps"
}

export def test_decision_with_deps_blocked_by_failed_dep [] {
    with-tmp {|ws|
        state-node-start $ws "sgoyol-0" "containerd" --actor "s" --source "orchestrator"
        state-node-finish $ws "sgoyol-0" "containerd"
        let d = (state-node-decision-with-deps $ws "sgoyol-0" "kubernetes" ["etcd" "containerd"])
        assert ($d | str starts-with "blocked:")
        assert ($d | str contains "containerd")
        # Blocked state must be written to file
        let node = (state-node-get $ws "sgoyol-0" "kubernetes")
        assert equal $node.state "blocked"
        assert equal $node.blocker "containerd"
    }
    print "✓ state-node-decision-with-deps: failed dep → blocked, state written to file"
}

export def test_decision_with_deps_runs_when_all_deps_completed [] {
    with-tmp {|ws|
        state-node-start $ws "sgoyol-0" "etcd" --actor "s" --source "orchestrator"
        state-node-finish $ws "sgoyol-0" "etcd" --success
        state-node-start $ws "sgoyol-0" "containerd" --actor "s" --source "orchestrator"
        state-node-finish $ws "sgoyol-0" "containerd" --success
        let d = (state-node-decision-with-deps $ws "sgoyol-0" "kubernetes" ["etcd" "containerd"])
        assert equal $d "run"
    }
    print "✓ state-node-decision-with-deps: all deps completed → run"
}

# ─── log rolling ─────────────────────────────────────────────────────────────

export def test_log_rolling_keeps_last_50 [] {
    with-tmp {|ws|
        # Write 60 start/finish cycles — log should cap at 50
        for i in 1..60 {
            state-node-start $ws "sgoyol-0" "etcd" --actor "s" --source "orchestrator"
            state-node-finish $ws "sgoyol-0" "etcd" --success
            state-node-reset $ws "sgoyol-0" "etcd"
        }
        let node = (state-node-get $ws "sgoyol-0" "etcd")
        assert ($node.log | length) <= 50
    }
    print "✓ log rolling: capped at 50 entries after 60 cycles"
}

# ─── state-migrate-from-json ─────────────────────────────────────────────────

export def test_state_migrate_from_json [] {
    with-tmp {|ws|
        # Write a minimal .provisioning-state.json
        let json_content = {
            cluster:   "librecloud",
            timestamp: "2026-02-15 22:05:42",
            version:   "1.0.4",
            state: {
                servers: { "sgoyol-0": "12345678" }
            }
        }
        $json_content | to json | save ($ws | path join ".provisioning-state.json")

        state-migrate-from-json $ws

        assert (state-path $ws | path exists)
        let st = (state-read $ws)
        assert equal $st.cluster "librecloud"
        assert ($st.servers | columns | any {|c| $c == "sgoyol-0"})
        # Migrated servers must start as unknown, not completed
        assert equal ($st.servers."sgoyol-0".provider_state) "unknown"
    }
    print "✓ state-migrate-from-json: JSON → NCL, servers set to unknown"
}

export def test_state_migrate_errors_if_ncl_exists [] {
    with-tmp {|ws|
        let json_content = { cluster: "c", timestamp: "", version: "1.0", state: { servers: {} } }
        $json_content | to json | save ($ws | path join ".provisioning-state.json")
        # Pre-create NCL file — migration must error
        "existing" | save (state-path $ws)
        let result = (do { state-migrate-from-json $ws } | complete)
        assert ($result.exit_code != 0)
    }
    print "✓ state-migrate-from-json: errors if .ncl already exists"
}

# ─── state-server-sync ───────────────────────────────────────────────────────

export def test_state_server_sync_updates_provider_state [] {
    with-tmp {|ws|
        state-server-sync $ws "sgoyol-0" --provider-id "99" --provider-state "running"
        let st = (state-read $ws)
        assert equal ($st.servers."sgoyol-0".provider_id) "99"
        assert equal ($st.servers."sgoyol-0".provider_state) "running"
        assert ($st.servers."sgoyol-0".last_sync | is-not-empty)
    }
    print "✓ state-server-sync: updates provider_id, provider_state, last_sync"
}

export def test_state_server_sync_preserves_existing_taskservs [] {
    with-tmp {|ws|
        state-node-start $ws "sgoyol-0" "etcd" --actor "s" --source "orchestrator"
        state-node-finish $ws "sgoyol-0" "etcd" --success
        state-server-sync $ws "sgoyol-0" --provider-state "running"
        # etcd must still be completed after sync
        let node = (state-node-get $ws "sgoyol-0" "etcd")
        assert equal $node.state "completed"
    }
    print "✓ state-server-sync: does not overwrite existing taskserv states"
}
