package controllogic

import (
	"context"
	"io"
	"log/slog"
	"sync"
	"testing"
	"time"

	"github.com/uopi/uopi/internal/audit"
	"github.com/uopi/uopi/internal/broker"
	"github.com/uopi/uopi/internal/confmgr"
	"github.com/uopi/uopi/internal/datasource"
)

// writableSource is a minimal in-memory DataSource that records the last value
// written to each signal, so config-apply/read nodes can be tested end-to-end.
type writableSource struct {
	mu      sync.Mutex
	written map[string]any
}

func newWritableSource() *writableSource { return &writableSource{written: map[string]any{}} }

func (s *writableSource) Name() string                  { return "tgt" }
func (s *writableSource) Connect(context.Context) error { return nil }
func (s *writableSource) ListSignals(context.Context) ([]datasource.Metadata, error) {
	return nil, nil
}
func (s *writableSource) GetMetadata(_ context.Context, sig string) (datasource.Metadata, error) {
	return datasource.Metadata{Name: sig, Writable: true}, nil
}

// Subscribe delivers the signal's last-written value once (if any), so a
// one-shot ReadNow (used by config snapshot) resolves immediately.
func (s *writableSource) Subscribe(_ context.Context, sig string, ch chan<- datasource.Value) (datasource.CancelFunc, error) {
	s.mu.Lock()
	v, ok := s.written[sig]
	s.mu.Unlock()
	if ok {
		select {
		case ch <- datasource.Value{Data: v, Timestamp: time.Now()}:
		default:
		}
	}
	return func() {}, nil
}
func (s *writableSource) Write(_ context.Context, signal string, value any) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.written[signal] = value
	return nil
}
func (s *writableSource) History(context.Context, string, time.Time, time.Time, int) ([]datasource.Value, error) {
	return nil, datasource.ErrHistoryUnavailable
}

func (s *writableSource) get(signal string) (any, bool) {
	s.mu.Lock()
	defer s.mu.Unlock()
	v, ok := s.written[signal]
	return v, ok
}

// setupConfigEngine wires a broker (with a writable "tgt" source), a confmgr
// store seeded with a set + instance, and an Engine bound to both.
func setupConfigEngine(t *testing.T) (*Engine, *writableSource, string) {
	t.Helper()
	log := slog.New(slog.NewTextHandler(io.Discard, nil))
	ctx := context.Background()

	src := newWritableSource()
	brk := broker.New(ctx, log)
	brk.Register(src)

	cfg, err := confmgr.New(t.TempDir())
	if err != nil {
		t.Fatal("confmgr.New:", err)
	}
	set, err := cfg.CreateSet(confmgr.ConfigSet{
		Name: "tuning",
		Parameters: []confmgr.Parameter{
			{Key: "gain", DS: "tgt", Signal: "GAIN", Type: confmgr.TypeFloat, Default: 1.0},
			{Key: "offset", DS: "tgt", Signal: "OFFSET", Type: confmgr.TypeFloat, Default: 0.0},
		},
	}, "")
	if err != nil {
		t.Fatal("CreateSet:", err)
	}
	inst, err := cfg.CreateInstance(confmgr.ConfigInstance{
		Name:   "warm",
		SetID:  set.ID,
		Values: map[string]any{"gain": 2.5, "offset": 10.0},
	}, "")
	if err != nil {
		t.Fatal("CreateInstance:", err)
	}

	e := NewEngine(ctx, brk, nil, cfg, audit.Nop(), log)
	return e, src, inst.ID
}

func TestApplyConfigWritesEveryParam(t *testing.T) {
	e, src, instID := setupConfigEngine(t)
	cg := &compiledGraph{name: "flow", engine: e, locals: map[string]float64{}}

	e.applyConfig(cg, instID)

	if got, ok := src.get("GAIN"); !ok || toNum(got) != 2.5 {
		t.Errorf("GAIN = %v (ok=%v), want 2.5", got, ok)
	}
	if got, ok := src.get("OFFSET"); !ok || toNum(got) != 10.0 {
		t.Errorf("OFFSET = %v (ok=%v), want 10.0", got, ok)
	}
}

func TestApplyConfigUnknownInstanceNoop(t *testing.T) {
	e, src, _ := setupConfigEngine(t)
	cg := &compiledGraph{name: "flow", engine: e, locals: map[string]float64{}}

	e.applyConfig(cg, "does-not-exist")

	if len(src.written) != 0 {
		t.Errorf("expected no writes, got %v", src.written)
	}
}

func TestReadConfigParam(t *testing.T) {
	e, _, instID := setupConfigEngine(t)

	if v, ok := e.readConfigParam(instID, "gain"); !ok || v != 2.5 {
		t.Errorf("readConfigParam(gain) = %v (ok=%v), want 2.5", v, ok)
	}
	// Missing param key.
	if _, ok := e.readConfigParam(instID, "nope"); ok {
		t.Errorf("readConfigParam(nope) returned ok=true, want false")
	}
	// Missing instance.
	if _, ok := e.readConfigParam("does-not-exist", "gain"); ok {
		t.Errorf("readConfigParam(missing instance) returned ok=true, want false")
	}
}

func TestWriteConfigParam(t *testing.T) {
	e, _, instID := setupConfigEngine(t)
	cg := &compiledGraph{name: "flow", engine: e, locals: map[string]float64{}}

	e.writeConfigParam(cg, instID, "gain", 7.5)

	// A new revision should now resolve to the written value.
	if v, ok := e.readConfigParam(instID, "gain"); !ok || v != 7.5 {
		t.Errorf("after write, gain = %v (ok=%v), want 7.5", v, ok)
	}
	inst, err := e.cfg.GetInstance(instID)
	if err != nil {
		t.Fatal("GetInstance:", err)
	}
	if inst.Version < 2 {
		t.Errorf("instance version = %d, want >= 2 (a new revision)", inst.Version)
	}
}

func TestCreateConfigInstance(t *testing.T) {
	e, _, srcID := setupConfigEngine(t)
	cg := &compiledGraph{name: "flow", engine: e, locals: map[string]float64{}}

	src, err := e.cfg.GetInstance(srcID)
	if err != nil {
		t.Fatal("GetInstance:", err)
	}

	e.createConfigInstance(cg, src.SetID, "cold", srcID)

	insts, err := e.cfg.List(confmgr.KindInstance)
	if err != nil {
		t.Fatal("List:", err)
	}
	var found *confmgr.ConfigInstance
	for i := range insts {
		if insts[i].Name == "cold" {
			full, err := e.cfg.GetInstance(insts[i].ID)
			if err != nil {
				t.Fatal("GetInstance:", err)
			}
			found = &full
		}
	}
	if found == nil {
		t.Fatal("created instance 'cold' not found")
	}
	// Values copied from the source instance.
	if got := toNum(found.Values["gain"]); got != 2.5 {
		t.Errorf("copied gain = %v, want 2.5", got)
	}
}

func TestSnapshotConfig(t *testing.T) {
	e, src, instID := setupConfigEngine(t)
	cg := &compiledGraph{name: "flow", engine: e, locals: map[string]float64{}}

	// Seed current live values for the set's target signals.
	_ = src.Write(context.Background(), "GAIN", 3.5)
	_ = src.Write(context.Background(), "OFFSET", -2.0)

	seed, err := e.cfg.GetInstance(instID)
	if err != nil {
		t.Fatal("GetInstance:", err)
	}
	e.snapshotConfig(cg, seed.SetID, "snap1")

	insts, err := e.cfg.List(confmgr.KindInstance)
	if err != nil {
		t.Fatal("List:", err)
	}
	var found *confmgr.ConfigInstance
	for i := range insts {
		if insts[i].Name == "snap1" {
			full, err := e.cfg.GetInstance(insts[i].ID)
			if err != nil {
				t.Fatal("GetInstance:", err)
			}
			found = &full
		}
	}
	if found == nil {
		t.Fatal("snapshot instance 'snap1' not found")
	}
	if got := toNum(found.Values["gain"]); got != 3.5 {
		t.Errorf("snapshot gain = %v, want 3.5 (current live value)", got)
	}
	if got := toNum(found.Values["offset"]); got != -2.0 {
		t.Errorf("snapshot offset = %v, want -2.0 (current live value)", got)
	}
}
