//go:build !epics

// Integration tests for the pure-Go EPICS datasource adapter.
// These tests use the in-process fake CA server from the goca testca package
// and therefore do not require a real EPICS installation.
package epics_test

import (
	"context"
	"math"
	"testing"
	"time"

	"github.com/uopi/goca/proto"
	"github.com/uopi/goca/testca"
	"github.com/uopi/uopi/internal/datasource"
	"github.com/uopi/uopi/internal/datasource/epics"
)

// newTestDS creates a datasource.DataSource backed by a fake CA server.
func newTestDS(t *testing.T, pvs []testca.PVSpec) (datasource.DataSource, *testca.Server) {
	t.Helper()
	srv, err := testca.New(pvs)
	if err != nil {
		t.Fatalf("testca.New: %v", err)
	}
	t.Cleanup(srv.Close)

	ds := epics.New(srv.UDPAddr(), "", "", "", false, nil)
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	t.Cleanup(cancel)
	if err := ds.Connect(ctx); err != nil {
		t.Fatalf("Connect: %v", err)
	}
	return ds, srv
}

// -------------------------------------------------------------------------- //
// GetMetadata                                                                  //
// -------------------------------------------------------------------------- //

func TestPureGo_GetMetadataDouble(t *testing.T) {
	ds, _ := newTestDS(t, []testca.PVSpec{
		{Name: "DS:DOUBLE", DBFType: proto.DBFDouble, Count: 1, Value: 1.0},
	})

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	meta, err := ds.GetMetadata(ctx, "DS:DOUBLE")
	if err != nil {
		t.Fatalf("GetMetadata: %v", err)
	}
	if meta.Name != "DS:DOUBLE" {
		t.Errorf("Name = %q, want %q", meta.Name, "DS:DOUBLE")
	}
	if meta.Type != datasource.TypeFloat64 {
		t.Errorf("Type = %v, want TypeFloat64", meta.Type)
	}
	if !meta.Writable {
		t.Error("expected Writable=true")
	}
}

func TestPureGo_GetMetadataString(t *testing.T) {
	ds, _ := newTestDS(t, []testca.PVSpec{
		{Name: "DS:STR", DBFType: proto.DBFString, Count: 1, Value: "hello"},
	})

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	meta, err := ds.GetMetadata(ctx, "DS:STR")
	if err != nil {
		t.Fatalf("GetMetadata: %v", err)
	}
	if meta.Type != datasource.TypeString {
		t.Errorf("Type = %v, want TypeString", meta.Type)
	}
}

func TestPureGo_GetMetadataEnum(t *testing.T) {
	ds, _ := newTestDS(t, []testca.PVSpec{
		{Name: "DS:ENUM", DBFType: proto.DBFEnum, Count: 1, Value: int16(0)},
	})

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	meta, err := ds.GetMetadata(ctx, "DS:ENUM")
	if err != nil {
		t.Fatalf("GetMetadata: %v", err)
	}
	if meta.Type != datasource.TypeEnum {
		t.Errorf("Type = %v, want TypeEnum", meta.Type)
	}
}

func TestPureGo_GetMetadataReadOnly(t *testing.T) {
	ds, _ := newTestDS(t, []testca.PVSpec{
		{Name: "DS:RO", DBFType: proto.DBFDouble, Count: 1, Value: 0.0,
			Access: proto.AccessRead},
	})

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	meta, err := ds.GetMetadata(ctx, "DS:RO")
	if err != nil {
		t.Fatalf("GetMetadata: %v", err)
	}
	if meta.Writable {
		t.Error("expected Writable=false for read-only PV")
	}
}

func TestPureGo_GetMetadataCached(t *testing.T) {
	ds, _ := newTestDS(t, []testca.PVSpec{
		{Name: "DS:CACHED", DBFType: proto.DBFDouble, Count: 1, Value: 1.0},
	})

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	m1, err := ds.GetMetadata(ctx, "DS:CACHED")
	if err != nil {
		t.Fatalf("first GetMetadata: %v", err)
	}
	m2, err := ds.GetMetadata(ctx, "DS:CACHED")
	if err != nil {
		t.Fatalf("second GetMetadata: %v", err)
	}
	if m1.Name != m2.Name || m1.Type != m2.Type {
		t.Error("second GetMetadata returned different result from cache")
	}
}

// -------------------------------------------------------------------------- //
// Subscribe                                                                    //
// -------------------------------------------------------------------------- //

func TestPureGo_SubscribeInitialValue(t *testing.T) {
	ds, _ := newTestDS(t, []testca.PVSpec{
		{Name: "DS:SUB", DBFType: proto.DBFDouble, Count: 1, Value: 7.5},
	})

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	ch := make(chan datasource.Value, 16)
	unsub, err := ds.Subscribe(ctx, "DS:SUB", ch)
	if err != nil {
		t.Fatalf("Subscribe: %v", err)
	}
	defer unsub()

	// Drain MetaUpdate and value frames — first numeric value wins.
	deadline := time.After(5 * time.Second)
	for {
		select {
		case v := <-ch:
			if v.MetaUpdate {
				continue
			}
			f, ok := v.Data.(float64)
			if !ok {
				t.Fatalf("Data type = %T, want float64", v.Data)
			}
			if math.Abs(f-7.5) > 1e-6 {
				t.Errorf("Data = %g, want 7.5", f)
			}
			return
		case <-deadline:
			t.Fatal("timeout waiting for initial value")
		}
	}
}

func TestPureGo_SubscribeUpdates(t *testing.T) {
	ds, srv := newTestDS(t, []testca.PVSpec{
		{Name: "DS:UPD", DBFType: proto.DBFDouble, Count: 1, Value: 1.0},
	})

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	ch := make(chan datasource.Value, 16)
	unsub, err := ds.Subscribe(ctx, "DS:UPD", ch)
	if err != nil {
		t.Fatalf("Subscribe: %v", err)
	}
	defer unsub()

	// Drain initial frames.
	deadline := time.After(3 * time.Second)
drainLoop:
	for {
		select {
		case v := <-ch:
			if !v.MetaUpdate {
				break drainLoop
			}
		case <-deadline:
			t.Fatal("timeout draining initial frames")
		}
	}

	// Push new value via server.
	srv.SetValue("DS:UPD", 99.9)

	deadline = time.After(3 * time.Second)
	for {
		select {
		case v := <-ch:
			if v.MetaUpdate {
				continue
			}
			f, ok := v.Data.(float64)
			if !ok {
				t.Fatalf("Data type = %T, want float64", v.Data)
			}
			if math.Abs(f-99.9) > 1e-6 {
				t.Errorf("Data = %g, want 99.9", f)
			}
			return
		case <-deadline:
			t.Fatal("timeout waiting for updated value")
		}
	}
}

// -------------------------------------------------------------------------- //
// Write                                                                        //
// -------------------------------------------------------------------------- //

func TestPureGo_WriteDouble(t *testing.T) {
	ds, _ := newTestDS(t, []testca.PVSpec{
		{Name: "DS:W", DBFType: proto.DBFDouble, Count: 1, Value: 0.0},
	})

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	ch := make(chan datasource.Value, 16)
	unsub, err := ds.Subscribe(ctx, "DS:W", ch)
	if err != nil {
		t.Fatalf("Subscribe: %v", err)
	}
	defer unsub()

	// Drain initial.
	deadline := time.After(3 * time.Second)
drainLoop:
	for {
		select {
		case v := <-ch:
			if !v.MetaUpdate {
				break drainLoop
			}
		case <-deadline:
			t.Fatal("timeout draining initial frames")
		}
	}

	if err := ds.Write(ctx, "DS:W", float64(3.14)); err != nil {
		t.Fatalf("Write: %v", err)
	}

	deadline = time.After(3 * time.Second)
	for {
		select {
		case v := <-ch:
			if v.MetaUpdate {
				continue
			}
			f, ok := v.Data.(float64)
			if !ok {
				continue
			}
			if math.Abs(f-3.14) > 1e-6 {
				t.Errorf("post-write Data = %g, want 3.14", f)
			}
			return
		case <-deadline:
			t.Fatal("timeout waiting for post-write monitor update")
		}
	}
}

func TestPureGo_WriteReadOnly(t *testing.T) {
	ds, _ := newTestDS(t, []testca.PVSpec{
		{Name: "DS:WRO", DBFType: proto.DBFDouble, Count: 1, Value: 0.0,
			Access: proto.AccessRead},
	})

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	err := ds.Write(ctx, "DS:WRO", float64(1.0))
	if err == nil {
		t.Fatal("expected error writing to read-only PV")
	}
}

// -------------------------------------------------------------------------- //
// ListSignals                                                                  //
// -------------------------------------------------------------------------- //

func TestPureGo_ListSignals(t *testing.T) {
	ds, _ := newTestDS(t, []testca.PVSpec{
		{Name: "DS:LS1", DBFType: proto.DBFDouble, Count: 1, Value: 0.0},
		{Name: "DS:LS2", DBFType: proto.DBFDouble, Count: 1, Value: 0.0},
	})

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	// GetMetadata for each PV to populate the cache.
	for _, name := range []string{"DS:LS1", "DS:LS2"} {
		if _, err := ds.GetMetadata(ctx, name); err != nil {
			t.Fatalf("GetMetadata(%q): %v", name, err)
		}
	}

	sigs, err := ds.ListSignals(ctx)
	if err != nil {
		t.Fatalf("ListSignals: %v", err)
	}
	if len(sigs) < 2 {
		t.Errorf("ListSignals returned %d signals, want ≥ 2", len(sigs))
	}
}

// -------------------------------------------------------------------------- //
// History                                                                      //
// -------------------------------------------------------------------------- //

func TestPureGo_HistoryUnavailable(t *testing.T) {
	ds, _ := newTestDS(t, []testca.PVSpec{
		{Name: "DS:HIST", DBFType: proto.DBFDouble, Count: 1, Value: 0.0},
	})

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	_, err := ds.History(ctx, "DS:HIST", time.Now().Add(-time.Hour), time.Now(), 100)
	if err != datasource.ErrHistoryUnavailable {
		t.Errorf("History with no archiveURL: want ErrHistoryUnavailable, got %v", err)
	}
}
