package scpi

import (
	"bufio"
	"context"
	"net"
	"strings"
	"sync"
	"testing"
	"time"

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

// mockInstrument is an in-process line-based SCPI server. It answers queries
// from a fixed table and records commands that produce no response (writes).
type mockInstrument struct {
	ln net.Listener

	mu      sync.Mutex
	answers map[string]string // query → response
	writes  []string
}

func newMockInstrument(t *testing.T) *mockInstrument {
	t.Helper()
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen: %v", err)
	}
	m := &mockInstrument{ln: ln, answers: map[string]string{}}
	go m.serve()
	t.Cleanup(func() { ln.Close() })
	return m
}

func (m *mockInstrument) addr() string { return m.ln.Addr().String() }

func (m *mockInstrument) setAnswer(q, a string) {
	m.mu.Lock()
	m.answers[q] = a
	m.mu.Unlock()
}

func (m *mockInstrument) writeLog() []string {
	m.mu.Lock()
	defer m.mu.Unlock()
	return append([]string(nil), m.writes...)
}

func (m *mockInstrument) serve() {
	for {
		conn, err := m.ln.Accept()
		if err != nil {
			return
		}
		go m.handle(conn)
	}
}

func (m *mockInstrument) handle(conn net.Conn) {
	defer conn.Close()
	br := bufio.NewReader(conn)
	for {
		line, err := br.ReadString('\n')
		if err != nil {
			return
		}
		cmd := strings.TrimRight(line, "\r\n")
		m.mu.Lock()
		if strings.HasSuffix(cmd, "?") {
			resp, ok := m.answers[cmd]
			if !ok {
				resp = "0"
			}
			m.mu.Unlock()
			conn.Write([]byte(resp + "\n"))
			continue
		}
		m.writes = append(m.writes, cmd)
		m.mu.Unlock()
	}
}

func testCfg(addr string) Config {
	return Config{
		Enabled:        true,
		PollIntervalMs: 20,
		Instruments: []Instrument{{
			Name:    "dmm",
			Address: addr,
			Channels: []Channel{
				{Name: "volt", Query: "MEAS:VOLT?", WriteCmd: "VOLT %v", Type: "float", Unit: "V"},
				{Name: "id", Query: "*IDN?", Type: "string"},
				{Name: "n", Query: "COUNT?", Type: "int"},
				{Name: "out", Query: "OUTP?", WriteCmd: "OUTP", Type: "bool"},
			},
		}},
	}
}

func TestQueryTypes(t *testing.T) {
	srv := newMockInstrument(t)
	srv.setAnswer("MEAS:VOLT?", "12.34")
	srv.setAnswer("*IDN?", "ACME,DMM,1,2.0")
	srv.setAnswer("COUNT?", "7")
	srv.setAnswer("OUTP?", "ON")

	s, err := New(testCfg(srv.addr()))
	if err != nil {
		t.Fatalf("New: %v", err)
	}
	defer s.Close()

	v, err := s.readSignal(s.signals["dmm:volt"])
	if err != nil {
		t.Fatalf("read volt: %v", err)
	}
	if f, ok := v.Data.(float64); !ok || f < 12.33 || f > 12.35 {
		t.Errorf("volt = %v (%T), want 12.34", v.Data, v.Data)
	}

	id, _ := s.readSignal(s.signals["dmm:id"])
	if id.Data != "ACME,DMM,1,2.0" {
		t.Errorf("id = %v", id.Data)
	}

	n, _ := s.readSignal(s.signals["dmm:n"])
	if iv, ok := n.Data.(int64); !ok || iv != 7 {
		t.Errorf("n = %v (%T), want 7", n.Data, n.Data)
	}

	out, _ := s.readSignal(s.signals["dmm:out"])
	if b, ok := out.Data.(bool); !ok || !b {
		t.Errorf("out = %v, want true", out.Data)
	}
}

func TestWrite(t *testing.T) {
	srv := newMockInstrument(t)
	s, _ := New(testCfg(srv.addr()))
	defer s.Close()
	ctx := context.Background()

	if err := s.Write(ctx, "dmm:volt", 3.3); err != nil {
		t.Fatalf("write volt: %v", err)
	}
	// bool write uses a template with no verb → "OUTP <val>".
	if err := s.Write(ctx, "dmm:out", true); err != nil {
		t.Fatalf("write out: %v", err)
	}

	// Give the server a moment to record both writes.
	deadline := time.Now().Add(time.Second)
	for time.Now().Before(deadline) {
		if len(srv.writeLog()) >= 2 {
			break
		}
		time.Sleep(5 * time.Millisecond)
	}
	got := srv.writeLog()
	if len(got) != 2 || got[0] != "VOLT 3.3" || got[1] != "OUTP true" {
		t.Errorf("writes = %v, want [VOLT 3.3, OUTP true]", got)
	}
}

func TestWriteErrors(t *testing.T) {
	srv := newMockInstrument(t)
	s, _ := New(testCfg(srv.addr()))
	defer s.Close()
	ctx := context.Background()

	if err := s.Write(ctx, "dmm:missing", 1); err != datasource.ErrNotFound {
		t.Errorf("missing = %v, want ErrNotFound", err)
	}
	if err := s.Write(ctx, "dmm:id", 1); err != datasource.ErrNotWritable {
		t.Errorf("read-only = %v, want ErrNotWritable", err)
	}
}

func TestSubscribe(t *testing.T) {
	srv := newMockInstrument(t)
	srv.setAnswer("MEAS:VOLT?", "5.0")
	s, _ := New(testCfg(srv.addr()))
	defer s.Close()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	ch := make(chan datasource.Value, 4)
	stop, err := s.Subscribe(ctx, "dmm:volt", ch)
	if err != nil {
		t.Fatalf("subscribe: %v", err)
	}
	defer stop()

	select {
	case v := <-ch:
		if v.Quality != datasource.QualityGood {
			t.Errorf("quality = %v", v.Quality)
		}
		if f, ok := v.Data.(float64); !ok || f != 5.0 {
			t.Errorf("value = %v, want 5.0", v.Data)
		}
	case <-time.After(2 * time.Second):
		t.Fatal("timed out")
	}
}

func TestSubscribeBadQuality(t *testing.T) {
	cfg := testCfg("127.0.0.1:1") // refused
	s, _ := New(cfg)
	defer s.Close()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	ch := make(chan datasource.Value, 1)
	stop, _ := s.Subscribe(ctx, "dmm:volt", ch)
	defer stop()

	select {
	case v := <-ch:
		if v.Quality != datasource.QualityBad {
			t.Errorf("quality = %v, want bad", v.Quality)
		}
	case <-time.After(2 * time.Second):
		t.Fatal("timed out")
	}
}

func TestValidation(t *testing.T) {
	if _, err := New(Config{Instruments: []Instrument{{Name: "", Address: "x"}}}); err == nil {
		t.Error("want error for missing name")
	}
	if _, err := New(Config{Instruments: []Instrument{{Name: "a", Address: "x"}, {Name: "a", Address: "y"}}}); err == nil {
		t.Error("want error for duplicate instrument")
	}
	if _, err := New(Config{Instruments: []Instrument{{Name: "a", Address: "x", Transport: "vxi11"}}}); err == nil {
		t.Error("want error for unimplemented vxi11 transport")
	}
	if _, err := New(Config{Instruments: []Instrument{{Name: "a", Address: "x", Transport: "bogus"}}}); err == nil {
		t.Error("want error for unknown transport")
	}
	if _, err := New(Config{Instruments: []Instrument{{Name: "a", Address: "x", Channels: []Channel{{Name: "c"}}}}}); err == nil {
		t.Error("want error for channel without query")
	}
}

func TestFormatWrite(t *testing.T) {
	if got := formatWrite("VOLT %v", 3.3); got != "VOLT 3.3" {
		t.Errorf("verb template = %q", got)
	}
	if got := formatWrite("OUTP", true); got != "OUTP true" {
		t.Errorf("plain template = %q", got)
	}
}

func TestParseValue(t *testing.T) {
	if v, _ := parseValue(datasource.TypeBool, "OFF"); v != false {
		t.Errorf("OFF = %v, want false", v)
	}
	if v, _ := parseValue(datasource.TypeBool, "2.0"); v != true {
		t.Errorf("2.0 bool = %v, want true", v)
	}
	if _, err := parseValue(datasource.TypeFloat64, "notnum"); err == nil {
		t.Error("want parse error for non-numeric float")
	}
}
