package controllogic

import (
	"context"
	"sync"
	"testing"
	"time"
)

// fakeObserver records every DebugEvent it receives, in order.
type fakeObserver struct {
	mu     sync.Mutex
	events []DebugEvent
}

func (f *fakeObserver) Observe(ev DebugEvent) {
	f.mu.Lock()
	f.events = append(f.events, ev)
	f.mu.Unlock()
}

func (f *fakeObserver) snapshot() []DebugEvent {
	f.mu.Lock()
	defer f.mu.Unlock()
	return append([]DebugEvent(nil), f.events...)
}

func (f *fakeObserver) last(nodeID string) (DebugEvent, bool) {
	f.mu.Lock()
	defer f.mu.Unlock()
	for i := len(f.events) - 1; i >= 0; i-- {
		if f.events[i].NodeID == nodeID {
			return f.events[i], true
		}
	}
	return DebugEvent{}, false
}

// thresholdWriteGraph is a 2-node flow: a timer trigger → an action.write of 42
// to "tgt:OUT". Used by both the observe and simulate tests.
func thresholdWriteGraph(id string) Graph {
	return Graph{
		ID:   id,
		Name: "flow",
		Nodes: []Node{
			{ID: "t", Kind: "trigger.timer", Params: map[string]string{"interval": "100"}},
			{ID: "w", Kind: "action.write", Params: map[string]string{"target": "tgt:OUT", "expr": "42"}},
		},
		Wires: []Wire{{From: "t", To: "w"}},
	}
}

// TestDebugObserverCapturesNodeValues drives a flow directly and asserts the
// observer saw the trigger fire and the write node's value — and that the real
// data-source write still happened (live mode, not dry-run).
func TestDebugObserverCapturesNodeValues(t *testing.T) {
	e, src, _ := setupConfigEngine(t)
	obs := &fakeObserver{}
	e.SetDebugObserver(obs)
	e.SetDebugWatch(map[string]bool{"g1": true})

	cg := compile(thresholdWriteGraph("g1"))
	cg.engine = e
	cg.genCtx = context.Background()
	cg.wg = &sync.WaitGroup{}

	cg.activate("t")
	cg.wg.Wait()

	events := obs.snapshot()
	if len(events) == 0 {
		t.Fatal("observer captured no events")
	}
	// Trigger then write must both be reported.
	if _, ok := obs.last("t"); !ok {
		t.Error("no event for trigger node 't'")
	}
	wEv, ok := obs.last("w")
	if !ok {
		t.Fatal("no event for write node 'w'")
	}
	if !wEv.HasValue || wEv.Value != 42 {
		t.Errorf("write node event = %+v, want value 42 hasValue true", wEv)
	}
	if wEv.GraphID != "g1" {
		t.Errorf("event GraphID = %q, want g1", wEv.GraphID)
	}
	// Live mode: the real write went through.
	if got, ok := src.get("OUT"); !ok || toNum(got) != 42 {
		t.Errorf("OUT = %v (ok=%v), want 42 written", got, ok)
	}
}

// manualWriteGraph is a flow whose trigger never fires on its own (a threshold
// with no signal wired) → an action.write of 42 to "tgt:OUT". Used to prove
// FireTrigger forces a run that would otherwise never happen.
func manualWriteGraph(id string) Graph {
	return Graph{
		ID:   id,
		Name: "flow",
		Nodes: []Node{
			{ID: "t", Kind: "trigger.threshold", Params: map[string]string{"op": ">", "value": "0"}},
			{ID: "w", Kind: "action.write", Params: map[string]string{"target": "tgt:OUT", "expr": "42"}},
		},
		Wires: []Wire{{From: "t", To: "w"}},
	}
}

// TestFireTriggerForcesRun verifies FireTrigger drives a flow whose trigger
// never fires on its own, routed through the simulate sandbox.
func TestFireTriggerForcesRun(t *testing.T) {
	e, _, _ := setupConfigEngine(t)
	obs := &fakeObserver{}
	e.SetDebugObserver(obs)
	e.SetDebugWatch(map[string]bool{})

	stop := e.StartSimulate(manualWriteGraph("sim"))
	defer stop()

	// Nothing should have run yet — the threshold trigger has no signal.
	time.Sleep(50 * time.Millisecond)
	if _, ok := obs.last("w"); ok {
		t.Fatal("write node ran before any manual fire")
	}

	if !e.FireTrigger("sim", "t") {
		t.Fatal("FireTrigger returned false for an active simulate route")
	}

	// Poll for the write node event (the flow runs on its own goroutine).
	var wEv DebugEvent
	for i := 0; i < 100; i++ {
		if ev, ok := obs.last("w"); ok {
			wEv = ev
			break
		}
		time.Sleep(10 * time.Millisecond)
	}
	if !wEv.HasValue || wEv.Value != 42 {
		t.Errorf("write node event = %+v, want value 42 hasValue true", wEv)
	}

	// An unknown route id must be a no-op (best-effort, returns false).
	if e.FireTrigger("does-not-exist", "t") {
		t.Error("FireTrigger returned true for an unknown route")
	}
}

// TestDebugWatchGatesEmission verifies emitDebug is silent for graphs absent
// from the watch set, so unwatched live graphs cost nothing.
func TestDebugWatchGatesEmission(t *testing.T) {
	e, _, _ := setupConfigEngine(t)
	obs := &fakeObserver{}
	e.SetDebugObserver(obs)
	e.SetDebugWatch(map[string]bool{}) // nobody watching

	cg := compile(thresholdWriteGraph("g1"))
	cg.engine = e
	cg.genCtx = context.Background()
	cg.wg = &sync.WaitGroup{}

	cg.activate("t")
	cg.wg.Wait()

	if got := obs.snapshot(); len(got) != 0 {
		t.Errorf("observer received %d events for an unwatched graph, want 0", len(got))
	}
}

// TestSimulateSuppressesWrites runs the graph through the dry-run sandbox and
// asserts node events are still emitted (alwaysDebug) but NO real write occurs.
func TestSimulateSuppressesWrites(t *testing.T) {
	e, src, _ := setupConfigEngine(t)
	obs := &fakeObserver{}
	e.SetDebugObserver(obs)
	// Deliberately leave the watch set empty: simulate must emit regardless.
	e.SetDebugWatch(map[string]bool{})

	stop := e.StartSimulate(thresholdWriteGraph("sim"))
	time.Sleep(250 * time.Millisecond) // let the 100 ms timer fire at least once
	stop()

	if _, ok := src.get("OUT"); ok {
		t.Errorf("simulate performed a real write to OUT, want none")
	}
	wEv, ok := obs.last("w")
	if !ok {
		t.Fatal("simulate produced no event for write node 'w'")
	}
	if !wEv.HasValue || wEv.Value != 42 {
		t.Errorf("simulate write event = %+v, want value 42 hasValue true", wEv)
	}
	if wEv.GraphID != "sim" {
		t.Errorf("simulate event GraphID = %q, want sim", wEv.GraphID)
	}
}
