package ca_test

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

	"github.com/uopi/goca"
	"github.com/uopi/goca/proto"
	"github.com/uopi/goca/testca"
)

// newTestClient creates a Client pointing only at the fake server's addresses.
func newTestClient(t *testing.T, srv *testca.Server) *ca.Client {
	t.Helper()
	cfg := ca.Config{
		AddrList:     []string{srv.UDPAddr()},
		AutoAddrList: false,
		ClientName:   "testclient",
		HostName:     "localhost",
	}
	cli, err := ca.NewClient(context.Background(), cfg)
	if err != nil {
		t.Fatalf("NewClient: %v", err)
	}
	t.Cleanup(cli.Close)
	return cli
}

// newTestServer creates a fake CA server with standard test PVs.
func newTestServer(t *testing.T) *testca.Server {
	t.Helper()
	srv, err := testca.New([]testca.PVSpec{
		{Name: "TEST:DOUBLE", DBFType: proto.DBFDouble, Count: 1, Value: 3.14},
		{Name: "TEST:LONG", DBFType: proto.DBFLong, Count: 1, Value: int32(42)},
		{Name: "TEST:STRING", DBFType: proto.DBFString, Count: 1, Value: "hello"},
		{Name: "TEST:ENUM", DBFType: proto.DBFEnum, Count: 1, Value: int16(1)},
		{Name: "TEST:READONLY", DBFType: proto.DBFDouble, Count: 1, Value: 1.0,
			Access: proto.AccessRead},
	})
	if err != nil {
		t.Fatalf("testca.New: %v", err)
	}
	t.Cleanup(srv.Close)
	return srv
}

// -------------------------------------------------------------------------- //
// Get tests                                                                    //
// -------------------------------------------------------------------------- //

func TestGetDouble(t *testing.T) {
	srv := newTestServer(t)
	cli := newTestClient(t, srv)

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

	tv, err := cli.Get(ctx, "TEST:DOUBLE")
	if err != nil {
		t.Fatalf("Get: %v", err)
	}
	if math.Abs(tv.Double-3.14) > 1e-6 {
		t.Errorf("Double = %g, want ~3.14", tv.Double)
	}
	if tv.Timestamp.IsZero() {
		t.Error("Timestamp is zero")
	}
}

func TestGetLong(t *testing.T) {
	srv := newTestServer(t)
	cli := newTestClient(t, srv)

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

	tv, err := cli.Get(ctx, "TEST:LONG")
	if err != nil {
		t.Fatalf("Get: %v", err)
	}
	if tv.Long != 42 {
		t.Errorf("Long = %d, want 42", tv.Long)
	}
}

func TestGetString(t *testing.T) {
	srv := newTestServer(t)
	cli := newTestClient(t, srv)

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

	tv, err := cli.Get(ctx, "TEST:STRING")
	if err != nil {
		t.Fatalf("Get: %v", err)
	}
	if tv.Str != "hello" {
		t.Errorf("Str = %q, want %q", tv.Str, "hello")
	}
}

func TestGetNotFound(t *testing.T) {
	srv := newTestServer(t)
	cli := newTestClient(t, srv)

	ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
	defer cancel()

	_, err := cli.Get(ctx, "NO:SUCH:PV")
	if err == nil {
		t.Fatal("expected error for missing PV, got nil")
	}
}

// -------------------------------------------------------------------------- //
// Subscribe tests                                                              //
// -------------------------------------------------------------------------- //

func TestSubscribeReceivesInitialValue(t *testing.T) {
	srv := newTestServer(t)
	cli := newTestClient(t, srv)

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

	ch := make(chan proto.TimeValue, 8)
	unsub, err := cli.Subscribe(ctx, "TEST:DOUBLE", ch)
	if err != nil {
		t.Fatalf("Subscribe: %v", err)
	}
	defer unsub()

	select {
	case tv := <-ch:
		if math.Abs(tv.Double-3.14) > 1e-6 {
			t.Errorf("initial Double = %g, want ~3.14", tv.Double)
		}
	case <-ctx.Done():
		t.Fatal("timeout waiting for initial monitor value")
	}
}

func TestSubscribeReceivesUpdates(t *testing.T) {
	srv := newTestServer(t)
	cli := newTestClient(t, srv)

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

	ch := make(chan proto.TimeValue, 8)
	unsub, err := cli.Subscribe(ctx, "TEST:DOUBLE", ch)
	if err != nil {
		t.Fatalf("Subscribe: %v", err)
	}
	defer unsub()

	// Drain initial value.
	select {
	case <-ch:
	case <-ctx.Done():
		t.Fatal("timeout waiting for initial value")
	}

	// Push a new value from the server side.
	if err := srv.SetValue("TEST:DOUBLE", 99.9); err != nil {
		t.Fatalf("SetValue: %v", err)
	}

	select {
	case tv := <-ch:
		if math.Abs(tv.Double-99.9) > 1e-6 {
			t.Errorf("updated Double = %g, want ~99.9", tv.Double)
		}
	case <-ctx.Done():
		t.Fatal("timeout waiting for updated monitor value")
	}
}

func TestUnsubscribeStopsUpdates(t *testing.T) {
	srv := newTestServer(t)
	cli := newTestClient(t, srv)

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

	ch := make(chan proto.TimeValue, 8)
	unsub, err := cli.Subscribe(ctx, "TEST:DOUBLE", ch)
	if err != nil {
		t.Fatalf("Subscribe: %v", err)
	}

	// Drain initial value.
	select {
	case <-ch:
	case <-ctx.Done():
		t.Fatal("timeout waiting for initial value")
	}

	unsub() // unsubscribe

	// Give the EVENT_CANCEL message time to be delivered.
	time.Sleep(50 * time.Millisecond)

	// Push a value; channel should NOT receive it.
	srv.SetValue("TEST:DOUBLE", 777.0)

	select {
	case tv := <-ch:
		// It's possible one update slipped through before cancel was processed.
		// Accept it, but not a second one.
		t.Logf("received one update after unsub (value=%g), checking for second...", tv.Double)
		select {
		case <-ch:
			t.Error("received second update after unsubscribe")
		case <-time.After(100 * time.Millisecond):
			// Good — no second update.
		}
	case <-time.After(150 * time.Millisecond):
		// No update received — correct.
	}
}

// -------------------------------------------------------------------------- //
// Put tests                                                                    //
// -------------------------------------------------------------------------- //

func TestPutDouble(t *testing.T) {
	srv := newTestServer(t)
	cli := newTestClient(t, srv)

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

	// Subscribe to observe the effect of Put.
	ch := make(chan proto.TimeValue, 8)
	unsub, err := cli.Subscribe(ctx, "TEST:DOUBLE", ch)
	if err != nil {
		t.Fatalf("Subscribe: %v", err)
	}
	defer unsub()

	// Drain initial value.
	select {
	case <-ch:
	case <-ctx.Done():
		t.Fatal("timeout waiting for initial value")
	}

	if err := cli.Put(ctx, "TEST:DOUBLE", float64(2.718)); err != nil {
		t.Fatalf("Put: %v", err)
	}

	select {
	case tv := <-ch:
		if math.Abs(tv.Double-2.718) > 1e-6 {
			t.Errorf("post-put Double = %g, want ~2.718", tv.Double)
		}
	case <-ctx.Done():
		t.Fatal("timeout waiting for post-put monitor value")
	}
}

func TestPutLong(t *testing.T) {
	srv := newTestServer(t)
	cli := newTestClient(t, srv)

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

	ch := make(chan proto.TimeValue, 8)
	unsub, err := cli.Subscribe(ctx, "TEST:LONG", ch)
	if err != nil {
		t.Fatalf("Subscribe: %v", err)
	}
	defer unsub()
	<-ch // drain initial

	if err := cli.Put(ctx, "TEST:LONG", int(100)); err != nil {
		t.Fatalf("Put: %v", err)
	}

	select {
	case tv := <-ch:
		if tv.Long != 100 {
			t.Errorf("post-put Long = %d, want 100", tv.Long)
		}
	case <-ctx.Done():
		t.Fatal("timeout waiting for post-put value")
	}
}

func TestPutString(t *testing.T) {
	srv := newTestServer(t)
	cli := newTestClient(t, srv)

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

	ch := make(chan proto.TimeValue, 8)
	unsub, err := cli.Subscribe(ctx, "TEST:STRING", ch)
	if err != nil {
		t.Fatalf("Subscribe: %v", err)
	}
	defer unsub()
	<-ch // drain initial

	if err := cli.Put(ctx, "TEST:STRING", "world"); err != nil {
		t.Fatalf("Put: %v", err)
	}

	select {
	case tv := <-ch:
		if tv.Str != "world" {
			t.Errorf("post-put Str = %q, want %q", tv.Str, "world")
		}
	case <-ctx.Done():
		t.Fatal("timeout waiting for post-put value")
	}
}

func TestPutReadOnly(t *testing.T) {
	srv := newTestServer(t)
	cli := newTestClient(t, srv)

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

	err := cli.Put(ctx, "TEST:READONLY", float64(1.0))
	if err == nil {
		t.Fatal("expected error writing to read-only PV")
	}
}

// -------------------------------------------------------------------------- //
// Multiple subscribers on the same PV                                         //
// -------------------------------------------------------------------------- //

func TestMultipleSubscribers(t *testing.T) {
	srv := newTestServer(t)
	cli := newTestClient(t, srv)

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

	const n = 3
	chs := make([]chan proto.TimeValue, n)
	for i := range chs {
		chs[i] = make(chan proto.TimeValue, 8)
		unsub, err := cli.Subscribe(ctx, "TEST:DOUBLE", chs[i])
		if err != nil {
			t.Fatalf("Subscribe[%d]: %v", i, err)
		}
		defer unsub()
	}

	// Drain initial values.
	for i, ch := range chs {
		select {
		case <-ch:
		case <-ctx.Done():
			t.Fatalf("timeout waiting for initial value on subscriber %d", i)
		}
	}

	// Push a new value.
	srv.SetValue("TEST:DOUBLE", 55.5)

	for i, ch := range chs {
		select {
		case tv := <-ch:
			if math.Abs(tv.Double-55.5) > 1e-6 {
				t.Errorf("subscriber %d: Double = %g, want 55.5", i, tv.Double)
			}
		case <-ctx.Done():
			t.Fatalf("timeout waiting for update on subscriber %d", i)
		}
	}
}

// -------------------------------------------------------------------------- //
// Context cancellation                                                         //
// -------------------------------------------------------------------------- //

func TestGetContextCancelled(t *testing.T) {
	srv := newTestServer(t)
	cli := newTestClient(t, srv)

	ctx, cancel := context.WithCancel(context.Background())
	cancel() // already cancelled

	_, err := cli.Get(ctx, "TEST:DOUBLE")
	if err == nil {
		t.Fatal("expected error with cancelled context")
	}
}
