package modbus

import (
	"context"
	"encoding/binary"
	"errors"
	"io"
	"net"
	"testing"
)

// serve runs a one-connection fake Modbus server. handle receives the request
// PDU and returns the reply PDU; returning nil closes the connection instead,
// which is how a mute or dying peer is simulated. The request frames the server
// saw are sent back on the returned channel once the connection ends.
func serve(t *testing.T, handle func(pdu []byte) []byte) (addr string, seen <-chan [][]byte) {
	t.Helper()
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen: %v", err)
	}
	t.Cleanup(func() { _ = ln.Close() })

	frames := make(chan [][]byte, 1)
	go func() {
		var got [][]byte
		defer func() { frames <- got }()
		conn, err := ln.Accept()
		if err != nil {
			return
		}
		defer conn.Close()
		for {
			var header [mbapLen]byte
			if _, err := io.ReadFull(conn, header[:]); err != nil {
				return
			}
			length := int(binary.BigEndian.Uint16(header[4:]))
			body := make([]byte, length-1)
			if _, err := io.ReadFull(conn, body); err != nil {
				return
			}
			got = append(got, append(append([]byte{}, header[:]...), body...))

			reply := handle(body)
			if reply == nil {
				return
			}
			out := make([]byte, mbapLen+len(reply))
			copy(out[0:], header[0:2]) // echo the transaction id
			binary.BigEndian.PutUint16(out[2:], protocolID)
			binary.BigEndian.PutUint16(out[4:], uint16(len(reply)+1))
			out[6] = header[6]
			copy(out[mbapLen:], reply)
			if _, err := conn.Write(out); err != nil {
				return
			}
		}
	}()
	return ln.Addr().String(), frames
}

func dial(t *testing.T, addr string) *Client {
	t.Helper()
	c, err := Connect(context.Background(), Options{Address: addr})
	if err != nil {
		t.Fatalf("connect: %v", err)
	}
	t.Cleanup(func() { _ = c.Close() })
	return c
}

// readReply builds a well-formed FC03 reply carrying the given register values.
func readReply(values ...uint16) []byte { return replyFor(fcReadHold, values...) }

// inputReply is the same for FC04, which frames its reply identically.
func inputReply(values ...uint16) []byte { return replyFor(fcReadInput, values...) }

func replyFor(fc byte, values ...uint16) []byte {
	out := []byte{fc, byte(len(values) * 2)}
	for _, v := range values {
		out = binary.BigEndian.AppendUint16(out, v)
	}
	return out
}

func TestReadHoldingDecodesRegisters(t *testing.T) {
	addr, seen := serve(t, func(pdu []byte) []byte {
		return readReply(0x0102, 0x0304, 0xFFFF)
	})
	c := dial(t, addr)

	got, err := c.ReadHolding(context.Background(), 20053, 3)
	if err != nil {
		t.Fatalf("ReadHolding: %v", err)
	}
	want := []uint16{0x0102, 0x0304, 0xFFFF}
	if len(got) != len(want) {
		t.Fatalf("got %d registers, want %d", len(got), len(want))
	}
	for i := range want {
		if got[i] != want[i] {
			t.Errorf("register %d = 0x%04x, want 0x%04x", i, got[i], want[i])
		}
	}

	_ = c.Close()
	frames := <-seen
	if len(frames) != 1 {
		t.Fatalf("server saw %d frames, want 1", len(frames))
	}
	f := frames[0]
	if pid := binary.BigEndian.Uint16(f[2:]); pid != protocolID {
		t.Errorf("protocol id = %d, want 0", pid)
	}
	// Length counts the unit id plus the five-byte PDU.
	if l := binary.BigEndian.Uint16(f[4:]); l != 6 {
		t.Errorf("length field = %d, want 6", l)
	}
	if f[6] != defaultUnitID {
		t.Errorf("unit id = %d, want %d", f[6], defaultUnitID)
	}
	if f[mbapLen] != fcReadHold {
		t.Errorf("function code = 0x%02x, want 0x%02x", f[mbapLen], fcReadHold)
	}
	if a := binary.BigEndian.Uint16(f[mbapLen+1:]); a != 20053 {
		t.Errorf("address = %d, want 20053", a)
	}
	if n := binary.BigEndian.Uint16(f[mbapLen+3:]); n != 3 {
		t.Errorf("quantity = %d, want 3", n)
	}
}

func TestReadInputAsksTheInputTable(t *testing.T) {
	addr, seen := serve(t, func(pdu []byte) []byte {
		// A charger that keeps its measurements in input registers answers FC04
		// and refuses FC03, so replying to the wrong code would hide a mix-up.
		if pdu[0] != fcReadInput {
			return []byte{pdu[0] | excMask, 0x02}
		}
		return inputReply(0x0102, 0x0304)
	})
	c := dial(t, addr)

	got, err := c.ReadInput(context.Background(), 20041, 2)
	if err != nil {
		t.Fatalf("ReadInput: %v", err)
	}
	if len(got) != 2 || got[0] != 0x0102 || got[1] != 0x0304 {
		t.Errorf("registers = %v, want [258 772]", got)
	}

	_ = c.Close()
	frames := <-seen
	if len(frames) != 1 {
		t.Fatalf("server saw %d frames, want 1", len(frames))
	}
	if fc := frames[0][mbapLen]; fc != fcReadInput {
		t.Errorf("function code = 0x%02x, want 0x%02x", fc, fcReadInput)
	}
	if a := binary.BigEndian.Uint16(frames[0][mbapLen+1:]); a != 20041 {
		t.Errorf("address = %d, want 20041", a)
	}
}

func TestReadInputRejectsHoldingReply(t *testing.T) {
	addr, _ := serve(t, func(pdu []byte) []byte { return readReply(0x0102) })
	c := dial(t, addr)

	if _, err := c.ReadInput(context.Background(), 20041, 1); err == nil {
		t.Fatal("expected an error when the reply echoes the other function code")
	}
}

func TestReadHoldingRejectsBadCounts(t *testing.T) {
	addr, _ := serve(t, func(pdu []byte) []byte { return readReply(0) })
	c := dial(t, addr)

	for _, count := range []uint16{0, maxReadRegs + 1} {
		if _, err := c.ReadHolding(context.Background(), 20000, count); err == nil {
			t.Errorf("count %d was accepted, want a refusal before the wire", count)
		}
	}
}

func TestReadHoldingRejectsShortPayload(t *testing.T) {
	// The server claims three registers but sends two.
	addr, _ := serve(t, func(pdu []byte) []byte {
		return []byte{fcReadHold, 6, 0x00, 0x01, 0x00, 0x02}
	})
	c := dial(t, addr)

	if _, err := c.ReadHolding(context.Background(), 20000, 3); err == nil {
		t.Fatal("a truncated payload was accepted")
	}
}

func TestWriteSingleEchoesAddressAndValue(t *testing.T) {
	addr, seen := serve(t, func(pdu []byte) []byte {
		return append([]byte{fcWriteReg}, pdu[1:5]...) // echo address + value
	})
	c := dial(t, addr)

	if err := c.WriteSingle(context.Background(), 21000, 1); err != nil {
		t.Fatalf("WriteSingle: %v", err)
	}

	_ = c.Close()
	frames := <-seen
	if len(frames) != 1 {
		t.Fatalf("server saw %d frames, want 1", len(frames))
	}
	f := frames[0]
	if f[mbapLen] != fcWriteReg {
		t.Errorf("function code = 0x%02x, want 0x%02x", f[mbapLen], fcWriteReg)
	}
	if a := binary.BigEndian.Uint16(f[mbapLen+1:]); a != 21000 {
		t.Errorf("address = %d, want 21000", a)
	}
	if v := binary.BigEndian.Uint16(f[mbapLen+3:]); v != 1 {
		t.Errorf("value = %d, want 1", v)
	}
}

// A charger that clamps a written value reports the clamp. That must surface as
// an error rather than be mistaken for the value having been applied.
func TestWriteSingleRejectsClampedValue(t *testing.T) {
	addr, _ := serve(t, func(pdu []byte) []byte {
		reply := append([]byte{fcWriteReg}, pdu[1:5]...)
		binary.BigEndian.PutUint16(reply[3:], 160) // clamped to 16.0 A
		return reply
	})
	c := dial(t, addr)

	err := c.WriteSingle(context.Background(), 21001, 320)
	if err == nil {
		t.Fatal("a clamped write was reported as successful")
	}
}

func TestWriteSingleRejectsWrongAddressEcho(t *testing.T) {
	addr, _ := serve(t, func(pdu []byte) []byte {
		reply := append([]byte{fcWriteReg}, pdu[1:5]...)
		binary.BigEndian.PutUint16(reply[1:], 29999)
		return reply
	})
	c := dial(t, addr)

	if err := c.WriteSingle(context.Background(), 21000, 1); err == nil {
		t.Fatal("a write echoing the wrong register was accepted")
	}
}

func TestExceptionReplyBecomesError(t *testing.T) {
	addr, _ := serve(t, func(pdu []byte) []byte {
		return []byte{fcReadHold | excMask, 0x02} // illegal data address
	})
	c := dial(t, addr)

	_, err := c.ReadHolding(context.Background(), 29999, 1)
	var mbErr *Error
	if !errors.As(err, &mbErr) {
		t.Fatalf("error = %v, want a *modbus.Error", err)
	}
	if mbErr.Function != fcReadHold {
		t.Errorf("Function = 0x%02x, want 0x%02x", mbErr.Function, fcReadHold)
	}
	if mbErr.Code != 0x02 {
		t.Errorf("Code = 0x%02x, want 0x02", mbErr.Code)
	}
}

// A reply carrying someone else's transaction id means the stream is out of
// step, so it must not be handed back as this request's answer.
func TestMismatchedTransactionIDIsRejected(t *testing.T) {
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen: %v", err)
	}
	defer ln.Close()
	go func() {
		conn, err := ln.Accept()
		if err != nil {
			return
		}
		defer conn.Close()
		var header [mbapLen]byte
		if _, err := io.ReadFull(conn, header[:]); err != nil {
			return
		}
		body := make([]byte, int(binary.BigEndian.Uint16(header[4:]))-1)
		if _, err := io.ReadFull(conn, body); err != nil {
			return
		}
		reply := readReply(1)
		out := make([]byte, mbapLen+len(reply))
		binary.BigEndian.PutUint16(out[0:], binary.BigEndian.Uint16(header[0:])+7) // wrong
		binary.BigEndian.PutUint16(out[2:], protocolID)
		binary.BigEndian.PutUint16(out[4:], uint16(len(reply)+1))
		out[6] = header[6]
		copy(out[mbapLen:], reply)
		_, _ = conn.Write(out)
	}()

	c := dial(t, ln.Addr().String())
	if _, err := c.ReadHolding(context.Background(), 20000, 1); err == nil {
		t.Fatal("a reply for another transaction was accepted")
	}
}

func TestClosedClientRefusesRequests(t *testing.T) {
	addr, _ := serve(t, func(pdu []byte) []byte { return readReply(0) })
	c := dial(t, addr)
	if err := c.Close(); err != nil {
		t.Fatalf("Close: %v", err)
	}
	if _, err := c.ReadHolding(context.Background(), 20000, 1); !errors.Is(err, ErrClosed) {
		t.Fatalf("error = %v, want ErrClosed", err)
	}
	// Close is deferred by callers and may also run on an error path.
	if err := c.Close(); err != nil {
		t.Fatalf("second Close: %v", err)
	}
}

func TestConnectRequiresAddress(t *testing.T) {
	if _, err := Connect(context.Background(), Options{}); err == nil {
		t.Fatal("an empty address was accepted")
	}
}
