package proto_test

import (
	"bytes"
	"encoding/binary"
	"math"
	"testing"
	"time"

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

// -------------------------------------------------------------------------- //
// Header round-trip                                                            //
// -------------------------------------------------------------------------- //

func TestHeaderRoundTrip(t *testing.T) {
	h := proto.Header{
		Command:     proto.CmdCreateChan,
		PayloadSize: 16,
		DataType:    proto.DBFDouble,
		DataCount:   1,
		Parameter1:  0xDEAD,
		Parameter2:  0xBEEF,
	}
	wire := h.Bytes()
	if len(wire) != proto.HeaderSize {
		t.Fatalf("Bytes() len = %d, want %d", len(wire), proto.HeaderSize)
	}

	got, n, err := proto.DecodeHeader(bytes.NewReader(wire))
	if err != nil {
		t.Fatalf("DecodeHeader: %v", err)
	}
	if n != proto.HeaderSize {
		t.Errorf("bytes consumed = %d, want %d", n, proto.HeaderSize)
	}
	if got.Command != h.Command {
		t.Errorf("Command = %d, want %d", got.Command, h.Command)
	}
	if got.DataType != h.DataType {
		t.Errorf("DataType = %d, want %d", got.DataType, h.DataType)
	}
	if got.Parameter1 != h.Parameter1 {
		t.Errorf("Parameter1 = %d, want %d", got.Parameter1, h.Parameter1)
	}
	if got.Parameter2 != h.Parameter2 {
		t.Errorf("Parameter2 = %d, want %d", got.Parameter2, h.Parameter2)
	}
}

func TestBuildMessage(t *testing.T) {
	payload := []byte("hello\x00\x00\x00") // 8 bytes (padded)
	h := proto.Header{Command: proto.CmdHostName, PayloadSize: 0}
	msg := proto.BuildMessage(h, payload)
	if len(msg) != proto.HeaderSize+len(payload) {
		t.Fatalf("len(msg) = %d, want %d", len(msg), proto.HeaderSize+len(payload))
	}
	// PayloadSize in wire should equal len(payload).
	wireSize := binary.BigEndian.Uint16(msg[2:4])
	if int(wireSize) != len(payload) {
		t.Errorf("wire PayloadSize = %d, want %d", wireSize, len(payload))
	}
}

func TestPadTo8(t *testing.T) {
	cases := [][2]int{{0, 0}, {1, 8}, {7, 8}, {8, 8}, {9, 16}, {16, 16}, {17, 24}}
	for _, c := range cases {
		if got := proto.PadTo8(c[0]); got != c[1] {
			t.Errorf("PadTo8(%d) = %d, want %d", c[0], got, c[1])
		}
	}
}

func TestBuildStringPayload(t *testing.T) {
	p := proto.BuildStringPayload("TEST")
	if len(p)%8 != 0 {
		t.Errorf("payload length %d not padded to 8", len(p))
	}
	if p[4] != 0 {
		t.Errorf("expected null terminator at index 4, got %d", p[4])
	}
}

// -------------------------------------------------------------------------- //
// DBR_TIME_* decoding                                                          //
// -------------------------------------------------------------------------- //

// buildTimeHeader builds the 12-byte common DBR_TIME header.
func buildTimeHeader(status, severity int16, sec, nsec uint32) []byte {
	b := make([]byte, 12)
	binary.BigEndian.PutUint16(b[0:], uint16(status))
	binary.BigEndian.PutUint16(b[2:], uint16(severity))
	binary.BigEndian.PutUint32(b[4:], sec)
	binary.BigEndian.PutUint32(b[8:], nsec)
	return b
}

func TestDecodeTimeDouble(t *testing.T) {
	const sec = 1_000_000
	const nsec = 500_000_000
	const want = 3.14159

	hdr := buildTimeHeader(0, 0, sec, nsec)
	pad := make([]byte, 4) // RISC pad
	val := make([]byte, 8)
	binary.BigEndian.PutUint64(val, math.Float64bits(want))
	payload := append(append(hdr, pad...), val...)

	tv, ok := proto.DecodeTimeValue(proto.DBRTimeDouble, 1, payload)
	if !ok {
		t.Fatal("DecodeTimeValue returned false")
	}
	if math.Abs(tv.Double-want) > 1e-10 {
		t.Errorf("Double = %g, want %g", tv.Double, want)
	}
	// EPICS epoch offset: 1990-01-01 00:00:00 UTC = Unix 631152000.
	wantUnix := int64(sec) + 631152000
	if tv.Timestamp.Unix() != wantUnix {
		t.Errorf("Timestamp.Unix() = %d, want %d", tv.Timestamp.Unix(), wantUnix)
	}
	if tv.Timestamp.Nanosecond() != int(nsec) {
		t.Errorf("Timestamp.Nanosecond() = %d, want %d", tv.Timestamp.Nanosecond(), nsec)
	}
}

func TestDecodeTimeLong(t *testing.T) {
	hdr := buildTimeHeader(0, 1, 0, 0)
	val := make([]byte, 4)
	binary.BigEndian.PutUint32(val, 0xFFFFFFD6) // -42 as two's complement
	payload := append(hdr, val...)

	tv, ok := proto.DecodeTimeValue(proto.DBRTimeLong, 1, payload)
	if !ok {
		t.Fatal("DecodeTimeValue returned false")
	}
	if tv.Long != -42 {
		t.Errorf("Long = %d, want -42", tv.Long)
	}
	if tv.Severity != proto.SeverityMinor {
		t.Errorf("Severity = %v, want Minor", tv.Severity)
	}
}

func TestDecodeTimeString(t *testing.T) {
	hdr := buildTimeHeader(0, 0, 0, 0)
	str := make([]byte, 40)
	copy(str, "hello")
	payload := append(hdr, str...)

	tv, ok := proto.DecodeTimeValue(proto.DBRTimeString, 1, payload)
	if !ok {
		t.Fatal("DecodeTimeValue returned false")
	}
	if tv.Str != "hello" {
		t.Errorf("Str = %q, want %q", tv.Str, "hello")
	}
}

func TestDecodeTimeEnum(t *testing.T) {
	hdr := buildTimeHeader(0, 0, 0, 0)
	val := []byte{0x00, 0x00, 0x00, 0x03} // 2-byte RISC pad + enum=3
	payload := append(hdr, val...)

	tv, ok := proto.DecodeTimeValue(proto.DBRTimeEnum, 1, payload)
	if !ok {
		t.Fatal("DecodeTimeValue returned false")
	}
	if tv.Enum != 3 {
		t.Errorf("Enum = %d, want 3", tv.Enum)
	}
}

func TestDecodeTimeWaveform(t *testing.T) {
	hdr := buildTimeHeader(0, 0, 0, 0)
	pad := make([]byte, 4)
	vals := []float64{1.0, 2.5, -0.5}
	valbytes := make([]byte, len(vals)*8)
	for i, v := range vals {
		binary.BigEndian.PutUint64(valbytes[i*8:], math.Float64bits(v))
	}
	payload := append(append(hdr, pad...), valbytes...)

	tv, ok := proto.DecodeTimeValue(proto.DBRTimeDouble, 3, payload)
	if !ok {
		t.Fatal("DecodeTimeValue returned false")
	}
	if len(tv.Doubles) != 3 {
		t.Fatalf("len(Doubles) = %d, want 3", len(tv.Doubles))
	}
	for i, want := range vals {
		if math.Abs(tv.Doubles[i]-want) > 1e-12 {
			t.Errorf("Doubles[%d] = %g, want %g", i, tv.Doubles[i], want)
		}
	}
}

func TestDecodeTimeTooShort(t *testing.T) {
	_, ok := proto.DecodeTimeValue(proto.DBRTimeDouble, 1, []byte{0, 1, 2})
	if ok {
		t.Error("expected false for short payload")
	}
}

// -------------------------------------------------------------------------- //
// DBR_CTRL_DOUBLE decoding                                                    //
// -------------------------------------------------------------------------- //

func TestDecodeCtrlDouble(t *testing.T) {
	p := make([]byte, 88)
	// status=0, severity=0, precision=3, pad=0, units="mA\0..."
	binary.BigEndian.PutUint16(p[4:], 3) // precision
	copy(p[8:], "mA")
	f64 := func(off int, v float64) {
		binary.BigEndian.PutUint64(p[off:], math.Float64bits(v))
	}
	f64(16, 100.0) // upper_disp
	f64(24, 0.0)   // lower_disp
	f64(32, 90.0)  // upper_alarm
	f64(40, 80.0)  // upper_warn
	f64(48, 20.0)  // lower_warn
	f64(56, 10.0)  // lower_alarm
	f64(64, 95.0)  // upper_ctrl
	f64(72, 5.0)   // lower_ctrl
	f64(80, 55.5)  // value

	cd, ok := proto.DecodeCtrlDouble(p)
	if !ok {
		t.Fatal("DecodeCtrlDouble returned false")
	}
	if cd.Units != "mA" {
		t.Errorf("Units = %q, want %q", cd.Units, "mA")
	}
	if cd.Precision != 3 {
		t.Errorf("Precision = %d, want 3", cd.Precision)
	}
	if math.Abs(cd.Value-55.5) > 1e-10 {
		t.Errorf("Value = %g, want 55.5", cd.Value)
	}
	if math.Abs(cd.UpperDispLimit-100.0) > 1e-10 {
		t.Errorf("UpperDispLimit = %g, want 100.0", cd.UpperDispLimit)
	}
}

func TestDecodeCtrlEnum(t *testing.T) {
	p := make([]byte, 424)
	binary.BigEndian.PutUint16(p[4:], 3) // no_str = 3
	strs := []string{"OFF", "ON", "FAULT"}
	for i, s := range strs {
		copy(p[6+i*26:], s)
	}
	binary.BigEndian.PutUint16(p[422:], 1) // value = 1

	ce, ok := proto.DecodeCtrlEnum(p)
	if !ok {
		t.Fatal("DecodeCtrlEnum returned false")
	}
	if len(ce.Strings) != 3 {
		t.Fatalf("len(Strings) = %d, want 3", len(ce.Strings))
	}
	if ce.Strings[2] != "FAULT" {
		t.Errorf("Strings[2] = %q, want FAULT", ce.Strings[2])
	}
	if ce.Value != 1 {
		t.Errorf("Value = %d, want 1", ce.Value)
	}
}

// -------------------------------------------------------------------------- //
// Put payload encoders                                                         //
// -------------------------------------------------------------------------- //

func TestEncodeDouble(t *testing.T) {
	b := proto.EncodeDouble(3.14)
	if len(b) != 8 {
		t.Fatalf("len = %d, want 8", len(b))
	}
	got := math.Float64frombits(binary.BigEndian.Uint64(b))
	if math.Abs(got-3.14) > 1e-15 {
		t.Errorf("decoded = %g, want 3.14", got)
	}
}

func TestEncodeLong(t *testing.T) {
	b := proto.EncodeLong(-1)
	if len(b) != 8 {
		t.Fatalf("len = %d, want 8", len(b))
	}
	got := int32(binary.BigEndian.Uint32(b))
	if got != -1 {
		t.Errorf("decoded = %d, want -1", got)
	}
}

func TestEncodeString(t *testing.T) {
	b := proto.EncodeString("hello")
	if len(b)%8 != 0 {
		t.Errorf("len %d not padded to 8", len(b))
	}
	if string(b[:5]) != "hello" {
		t.Errorf("content = %q, want %q", b[:5], "hello")
	}
}

func TestEncodeEventMask(t *testing.T) {
	b := proto.EncodeEventMask(proto.DBEDefault)
	if len(b) != 16 {
		t.Fatalf("len = %d, want 16", len(b))
	}
	// m_mask is at offset 12 per caProto.h struct mon_info.
	mask := binary.BigEndian.Uint16(b[12:])
	if mask != proto.DBEDefault {
		t.Errorf("mask = 0x%02X, want 0x%02X", mask, proto.DBEDefault)
	}
	// m_pad at [14:16] must be zero.
	if pad := binary.BigEndian.Uint16(b[14:]); pad != 0 {
		t.Errorf("m_pad = %d, want 0", pad)
	}
}

// -------------------------------------------------------------------------- //
// NativeTimeType / NativeCtrlType                                             //
// -------------------------------------------------------------------------- //

func TestNativeTimeType(t *testing.T) {
	cases := []struct {
		dbf  int
		n    int
		want uint16
	}{
		{proto.DBFDouble, 1, proto.DBRTimeDouble},
		{proto.DBFFloat, 1, proto.DBRTimeFloat},
		{proto.DBFLong, 1, proto.DBRTimeLong},
		{proto.DBFShort, 1, proto.DBRTimeShort},
		{proto.DBFEnum, 1, proto.DBRTimeEnum},
		{proto.DBFString, 1, proto.DBRTimeString},
		{proto.DBFChar, 1, proto.DBRTimeChar},
		{proto.DBFChar, 10, proto.DBRTimeString}, // char waveform → string
	}
	for _, c := range cases {
		got := proto.NativeTimeType(c.dbf, c.n)
		if got != c.want {
			t.Errorf("NativeTimeType(%d,%d) = %d, want %d", c.dbf, c.n, got, c.want)
		}
	}
}

// -------------------------------------------------------------------------- //
// AlarmSeverity.String                                                         //
// -------------------------------------------------------------------------- //

func TestAlarmSeverityString(t *testing.T) {
	cases := []struct {
		s    proto.AlarmSeverity
		want string
	}{
		{proto.SeverityNone, "NO_ALARM"},
		{proto.SeverityMinor, "MINOR"},
		{proto.SeverityMajor, "MAJOR"},
		{proto.SeverityInvalid, "INVALID"},
	}
	for _, c := range cases {
		if got := c.s.String(); got != c.want {
			t.Errorf("Severity(%d).String() = %q, want %q", c.s, got, c.want)
		}
	}
}

// -------------------------------------------------------------------------- //
// Timestamp sanity check                                                       //
// -------------------------------------------------------------------------- //

func TestEPICSEpoch(t *testing.T) {
	// secPastEpoch=0 should decode to 1990-01-01 00:00:00 UTC.
	hdr := buildTimeHeader(0, 0, 0, 0)
	val := make([]byte, 8)
	payload := append(append(hdr, make([]byte, 4)...), val...)

	tv, ok := proto.DecodeTimeValue(proto.DBRTimeDouble, 1, payload)
	if !ok {
		t.Fatal("DecodeTimeValue returned false")
	}
	want := time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC)
	if !tv.Timestamp.Equal(want) {
		t.Errorf("timestamp = %v, want %v", tv.Timestamp, want)
	}
}
