package synthetic

import (
	"context"
	"log/slog"
	"os"
	"path/filepath"
	"testing"
	"time"

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

func newTestSynthetic(t *testing.T) (*Synthetic, *broker.Broker, context.CancelFunc) {
	t.Helper()
	dir := t.TempDir()
	log := slog.New(slog.NewTextHandler(os.Stderr, nil))
	ctx, cancel := context.WithCancel(context.Background())
	brk := broker.New(ctx, log)
	syn := New(dir, brk, log)
	return syn, brk, cancel
}

// TestNameAndConnect verifies basic construction and Connect.
func TestNameAndConnect(t *testing.T) {
	syn, _, cancel := newTestSynthetic(t)
	defer cancel()

	if syn.Name() != "synthetic" {
		t.Errorf("Name: want %q, got %q", "synthetic", syn.Name())
	}

	ctx := context.Background()
	if err := syn.Connect(ctx); err != nil {
		t.Fatalf("Connect: %v", err)
	}
}

// TestConnectCreatesEmptyFile verifies that Connect creates an empty
// synthetic.json when none exists.
func TestConnectCreatesEmptyFile(t *testing.T) {
	syn, _, cancel := newTestSynthetic(t)
	defer cancel()

	if err := syn.Connect(context.Background()); err != nil {
		t.Fatal(err)
	}

	path := filepath.Join(syn.storePath, "synthetic.json")
	data, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("expected file to be created: %v", err)
	}
	if string(data) == "" {
		t.Error("file should not be empty")
	}
}

// TestAddAndRemoveSignal exercises CRUD at runtime.
func TestAddAndRemoveSignal(t *testing.T) {
	syn, _, cancel := newTestSynthetic(t)
	defer cancel()

	if err := syn.Connect(context.Background()); err != nil {
		t.Fatal(err)
	}

	def := SignalDef{
		Name:   "test_gain",
		DS:     "stub",
		Signal: "sine_1hz",
		Pipeline: []NodeDef{
			{Type: "gain", Params: map[string]any{"gain": 2.0}},
		},
		Meta: MetaOverride{Unit: "V"},
	}

	if err := syn.AddSignal(def); err != nil {
		t.Fatalf("AddSignal: %v", err)
	}

	// Duplicate should fail.
	if err := syn.AddSignal(def); err == nil {
		t.Error("expected error for duplicate signal")
	}

	// Should be listed.
	metas, err := syn.ListSignals(context.Background())
	if err != nil {
		t.Fatal(err)
	}
	if len(metas) != 1 || metas[0].Name != "test_gain" {
		t.Errorf("unexpected metas: %v", metas)
	}

	// GetMetadata should work.
	meta, err := syn.GetMetadata(context.Background(), "test_gain")
	if err != nil {
		t.Fatalf("GetMetadata: %v", err)
	}
	if meta.Unit != "V" {
		t.Errorf("unit: want V, got %q", meta.Unit)
	}

	// Remove it.
	if err := syn.RemoveSignal("test_gain"); err != nil {
		t.Fatalf("RemoveSignal: %v", err)
	}

	// Should be gone.
	if _, err := syn.GetMetadata(context.Background(), "test_gain"); err != datasource.ErrNotFound {
		t.Errorf("expected ErrNotFound after remove, got %v", err)
	}

	// Remove non-existing should return ErrNotFound.
	if err := syn.RemoveSignal("nonexistent"); err != datasource.ErrNotFound {
		t.Errorf("expected ErrNotFound, got %v", err)
	}
}

// TestAddSignalEmptyName validates that empty name is rejected.
func TestAddSignalEmptyName(t *testing.T) {
	syn, _, cancel := newTestSynthetic(t)
	defer cancel()

	if err := syn.Connect(context.Background()); err != nil {
		t.Fatal(err)
	}

	if err := syn.AddSignal(SignalDef{}); err == nil {
		t.Error("expected error for empty signal name")
	}
}

// TestGetMetadataNotFound verifies ErrNotFound for unknown signal.
func TestGetMetadataNotFound(t *testing.T) {
	syn, _, cancel := newTestSynthetic(t)
	defer cancel()

	if err := syn.Connect(context.Background()); err != nil {
		t.Fatal(err)
	}

	_, err := syn.GetMetadata(context.Background(), "no_such_signal")
	if err != datasource.ErrNotFound {
		t.Errorf("expected ErrNotFound, got %v", err)
	}
}

// TestWriteNotSupported ensures Write returns ErrNotWritable.
func TestWriteNotSupported(t *testing.T) {
	syn, _, cancel := newTestSynthetic(t)
	defer cancel()

	err := syn.Write(context.Background(), "any", 1.0)
	if err != datasource.ErrNotWritable {
		t.Errorf("expected ErrNotWritable, got %v", err)
	}
}

// TestHistoryNotSupported ensures History returns ErrHistoryUnavailable.
func TestHistoryNotSupported(t *testing.T) {
	syn, _, cancel := newTestSynthetic(t)
	defer cancel()

	_, err := syn.History(context.Background(), "any", time.Now(), time.Now(), 10)
	if err != datasource.ErrHistoryUnavailable {
		t.Errorf("expected ErrHistoryUnavailable, got %v", err)
	}
}

// TestPersistence verifies that definitions survive a restart (New+Connect).
func TestPersistence(t *testing.T) {
	dir := t.TempDir()
	log := slog.New(slog.NewTextHandler(os.Stderr, nil))

	ctx, cancel := context.WithCancel(context.Background())
	brk := broker.New(ctx, log)
	syn := New(dir, brk, log)

	if err := syn.Connect(ctx); err != nil {
		t.Fatal(err)
	}

	def := SignalDef{
		Name:   "persisted",
		DS:     "stub",
		Signal: "sine_1hz",
		Pipeline: []NodeDef{
			{Type: "offset", Params: map[string]any{"offset": 5.0}},
		},
	}
	if err := syn.AddSignal(def); err != nil {
		t.Fatal(err)
	}
	cancel()

	// Reload.
	ctx2 := t.Context()
	brk2 := broker.New(ctx2, log)
	syn2 := New(dir, brk2, log)
	if err := syn2.Connect(ctx2); err != nil {
		t.Fatal(err)
	}

	meta, err := syn2.GetMetadata(ctx2, "persisted")
	if err != nil {
		t.Fatalf("after reload, GetMetadata: %v", err)
	}
	if meta.Name != "persisted" {
		t.Errorf("expected name %q, got %q", "persisted", meta.Name)
	}
}

// TestSubscribeUnknownSignal verifies that subscribing to unknown signal returns error.
func TestSubscribeUnknownSignal(t *testing.T) {
	syn, _, cancel := newTestSynthetic(t)
	defer cancel()

	if err := syn.Connect(context.Background()); err != nil {
		t.Fatal(err)
	}

	ch := make(chan datasource.Value, 1)
	_, err := syn.Subscribe(context.Background(), "nonexistent", ch)
	if err != datasource.ErrNotFound {
		t.Errorf("expected ErrNotFound, got %v", err)
	}
}

// TestGetDefs verifies GetDefs returns current definitions.
func TestGetDefs(t *testing.T) {
	syn, _, cancel := newTestSynthetic(t)
	defer cancel()

	if err := syn.Connect(context.Background()); err != nil {
		t.Fatal(err)
	}

	defs := syn.GetDefs()
	if len(defs) != 0 {
		t.Errorf("expected empty defs, got %d", len(defs))
	}

	def := SignalDef{
		Name:   "test",
		DS:     "stub",
		Signal: "noise",
	}
	if err := syn.AddSignal(def); err != nil {
		t.Fatal(err)
	}

	defs = syn.GetDefs()
	if len(defs) != 1 || defs[0].Name != "test" {
		t.Errorf("unexpected defs: %v", defs)
	}
}
