package modbus

import (
	"context"
	"encoding/binary"
	"io"
	"net"
	"sync"
	"testing"
	"time"

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

// mockServer is an in-process Modbus TCP slave for tests. It serves a small
// register/coil store and records writes. Only the function codes exercised by
// the data source are implemented.
type mockServer struct {
	ln net.Listener

	mu        sync.Mutex
	holding   map[uint16]uint16
	input     map[uint16]uint16
	coils     map[uint16]bool
	discrete  map[uint16]bool
	lastWrite []uint16 // registers from the most recent write
}

func newMockServer(t *testing.T) *mockServer {
	t.Helper()
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen: %v", err)
	}
	s := &mockServer{
		ln:       ln,
		holding:  map[uint16]uint16{},
		input:    map[uint16]uint16{},
		coils:    map[uint16]bool{},
		discrete: map[uint16]bool{},
	}
	go s.serve()
	t.Cleanup(func() { ln.Close() })
	return s
}

func (s *mockServer) addr() string { return s.ln.Addr().String() }

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

func (s *mockServer) handle(conn net.Conn) {
	defer conn.Close()
	for {
		head := make([]byte, 7)
		if _, err := io.ReadFull(conn, head); err != nil {
			return
		}
		tx := binary.BigEndian.Uint16(head[0:])
		length := binary.BigEndian.Uint16(head[4:])
		body := make([]byte, length-1)
		if _, err := io.ReadFull(conn, body); err != nil {
			return
		}
		resp := s.respond(body)
		out := make([]byte, 7+len(resp))
		binary.BigEndian.PutUint16(out[0:], tx)
		binary.BigEndian.PutUint16(out[2:], 0)
		binary.BigEndian.PutUint16(out[4:], uint16(1+len(resp)))
		out[6] = head[6]
		copy(out[7:], resp)
		if _, err := conn.Write(out); err != nil {
			return
		}
	}
}

func (s *mockServer) respond(pdu []byte) []byte {
	s.mu.Lock()
	defer s.mu.Unlock()
	fc := pdu[0]
	switch fc {
	case fcReadHolding, fcReadInput:
		addr := binary.BigEndian.Uint16(pdu[1:])
		qty := binary.BigEndian.Uint16(pdu[3:])
		out := []byte{fc, byte(qty * 2)}
		src := s.holding
		if fc == fcReadInput {
			src = s.input
		}
		for i := uint16(0); i < qty; i++ {
			out = binary.BigEndian.AppendUint16(out, src[addr+i])
		}
		return out
	case fcReadCoils, fcReadDiscrete:
		addr := binary.BigEndian.Uint16(pdu[1:])
		qty := binary.BigEndian.Uint16(pdu[3:])
		nbytes := (int(qty) + 7) / 8
		out := []byte{fc, byte(nbytes)}
		bits := make([]byte, nbytes)
		src := s.coils
		if fc == fcReadDiscrete {
			src = s.discrete
		}
		for i := uint16(0); i < qty; i++ {
			if src[addr+i] {
				bits[i/8] |= 1 << (i % 8)
			}
		}
		return append(out, bits...)
	case fcWriteSingleReg:
		addr := binary.BigEndian.Uint16(pdu[1:])
		val := binary.BigEndian.Uint16(pdu[3:])
		s.holding[addr] = val
		s.lastWrite = []uint16{val}
		return pdu // echo
	case fcWriteSingleCoil:
		addr := binary.BigEndian.Uint16(pdu[1:])
		s.coils[addr] = binary.BigEndian.Uint16(pdu[3:]) == 0xFF00
		return pdu
	case fcWriteMultipleRegs:
		addr := binary.BigEndian.Uint16(pdu[1:])
		qty := binary.BigEndian.Uint16(pdu[3:])
		s.lastWrite = nil
		for i := uint16(0); i < qty; i++ {
			v := binary.BigEndian.Uint16(pdu[6+i*2:])
			s.holding[addr+i] = v
			s.lastWrite = append(s.lastWrite, v)
		}
		return append([]byte{fc}, pdu[1:5]...)
	default:
		return []byte{fc | 0x80, 0x01}
	}
}

func (s *mockServer) setHolding(addr, val uint16) {
	s.mu.Lock()
	s.holding[addr] = val
	s.mu.Unlock()
}

func (s *mockServer) setInput(addr, val uint16) {
	s.mu.Lock()
	s.input[addr] = val
	s.mu.Unlock()
}

func (s *mockServer) setDiscrete(addr uint16, on bool) {
	s.mu.Lock()
	s.discrete[addr] = on
	s.mu.Unlock()
}

func testConfig(addr string) Config {
	return Config{
		Enabled:        true,
		PollIntervalMs: 20,
		Devices: []Device{{
			Name:    "dev",
			Address: addr,
			UnitID:  1,
			Registers: []Register{
				{Name: "temp", Kind: "holding", Address: 10, Encoding: "int16", Scale: 0.1, Unit: "C", Writable: true},
				{Name: "count", Kind: "holding", Address: 20, Encoding: "uint16"},
				{Name: "big", Kind: "input", Address: 30, Encoding: "uint32"},
				{Name: "flag", Kind: "discrete", Address: 5},
				{Name: "relay", Kind: "coil", Address: 6, Writable: true},
				{Name: "sp", Kind: "holding", Address: 40, Encoding: "float32", Writable: true},
			},
		}},
	}
}

func TestReadRegisters(t *testing.T) {
	srv := newMockServer(t)
	srv.setHolding(10, 235) // int16, scale 0.1 → 23.5
	srv.setHolding(20, 7)
	srv.setInput(30, 0)
	srv.setInput(31, 1000) // uint32 big-word-first: low word at 31
	srv.setDiscrete(5, true)

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

	temp, err := m.readSignal(m.signals["dev:temp"])
	if err != nil {
		t.Fatalf("read temp: %v", err)
	}
	if f, ok := temp.Data.(float64); !ok || f < 23.49 || f > 23.51 {
		t.Errorf("temp = %v (%T), want 23.5", temp.Data, temp.Data)
	}

	count, err := m.readSignal(m.signals["dev:count"])
	if err != nil {
		t.Fatalf("read count: %v", err)
	}
	if v, ok := count.Data.(int64); !ok || v != 7 {
		t.Errorf("count = %v (%T), want int64 7", count.Data, count.Data)
	}

	big, err := m.readSignal(m.signals["dev:big"])
	if err != nil {
		t.Fatalf("read big: %v", err)
	}
	if v, ok := big.Data.(int64); !ok || v != 1000 {
		t.Errorf("big = %v (%T), want int64 1000", big.Data, big.Data)
	}

	flag, err := m.readSignal(m.signals["dev:flag"])
	if err != nil {
		t.Fatalf("read flag: %v", err)
	}
	if b, ok := flag.Data.(bool); !ok || !b {
		t.Errorf("flag = %v, want true", flag.Data)
	}
}

func TestWriteRoundTrip(t *testing.T) {
	srv := newMockServer(t)
	m, err := New(testConfig(srv.addr()))
	if err != nil {
		t.Fatalf("New: %v", err)
	}
	defer m.Close()
	ctx := context.Background()

	// Scaled int16: writing 23.5 with scale 0.1 should store raw 235.
	if err := m.Write(ctx, "dev:temp", 23.5); err != nil {
		t.Fatalf("write temp: %v", err)
	}
	srv.mu.Lock()
	raw := srv.holding[10]
	srv.mu.Unlock()
	if raw != 235 {
		t.Errorf("holding[10] = %d, want 235", raw)
	}

	// Coil write.
	if err := m.Write(ctx, "dev:relay", true); err != nil {
		t.Fatalf("write relay: %v", err)
	}
	srv.mu.Lock()
	on := srv.coils[6]
	srv.mu.Unlock()
	if !on {
		t.Error("coil 6 not set")
	}

	// float32 multi-register write.
	if err := m.Write(ctx, "dev:sp", 12.5); err != nil {
		t.Fatalf("write sp: %v", err)
	}
	got, err := m.readSignal(m.signals["dev:sp"])
	if err != nil {
		t.Fatalf("read sp: %v", err)
	}
	if f, ok := got.Data.(float64); !ok || f < 12.49 || f > 12.51 {
		t.Errorf("sp = %v, want 12.5", got.Data)
	}
}

func TestWriteErrors(t *testing.T) {
	srv := newMockServer(t)
	m, _ := New(testConfig(srv.addr()))
	defer m.Close()
	ctx := context.Background()

	if err := m.Write(ctx, "dev:missing", 1); err != datasource.ErrNotFound {
		t.Errorf("missing write err = %v, want ErrNotFound", err)
	}
	// Input register is read-only.
	if err := m.Write(ctx, "dev:big", 1); err != datasource.ErrNotWritable {
		t.Errorf("input write err = %v, want ErrNotWritable", err)
	}
}

func TestSubscribe(t *testing.T) {
	srv := newMockServer(t)
	srv.setHolding(20, 42)
	m, _ := New(testConfig(srv.addr()))
	defer m.Close()

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

	select {
	case v := <-ch:
		if v.Quality != datasource.QualityGood {
			t.Errorf("quality = %v, want good", v.Quality)
		}
		if iv, ok := v.Data.(int64); !ok || iv != 42 {
			t.Errorf("first value = %v, want 42", v.Data)
		}
	case <-time.After(2 * time.Second):
		t.Fatal("timed out waiting for first value")
	}
}

func TestSubscribeBadQualityOnError(t *testing.T) {
	// Point at a closed port so reads fail; expect QualityBad, not a hang.
	cfg := testConfig("127.0.0.1:1") // port 1: connection refused
	m, _ := New(cfg)
	defer m.Close()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	ch := make(chan datasource.Value, 1)
	stop, err := m.Subscribe(ctx, "dev:count", ch)
	if err != nil {
		t.Fatalf("subscribe: %v", err)
	}
	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 TestNewValidation(t *testing.T) {
	if _, err := New(Config{Devices: []Device{{Name: "", Address: "x"}}}); err == nil {
		t.Error("expected error for missing device name")
	}
	if _, err := New(Config{Devices: []Device{{Name: "a", Address: "x"}, {Name: "a", Address: "y"}}}); err == nil {
		t.Error("expected error for duplicate device")
	}
	if _, err := New(Config{Devices: []Device{{Name: "a", Address: "x", Registers: []Register{{Name: "r", Encoding: "bogus"}}}}}); err == nil {
		t.Error("expected error for bad encoding")
	}
	if _, err := New(Config{Devices: []Device{{Name: "a", Address: "x", Registers: []Register{{Name: "r"}, {Name: "r"}}}}}); err == nil {
		t.Error("expected error for duplicate register")
	}
}

func TestSubscribeUnknownSignal(t *testing.T) {
	m, _ := New(testConfig("127.0.0.1:502"))
	defer m.Close()
	if _, err := m.Subscribe(context.Background(), "nope", nil); err != datasource.ErrNotFound {
		t.Errorf("err = %v, want ErrNotFound", err)
	}
	if _, err := m.GetMetadata(context.Background(), "nope"); err != datasource.ErrNotFound {
		t.Errorf("meta err = %v, want ErrNotFound", err)
	}
}
