package udp

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

// What we send has to be what we can read back: Logger32 and the rest decode
// this with the same rules our own parser uses, so a round trip through
// ParseWSJT is the honest check.
//
// The case behind it: OpsLog reported "sent 1153 bytes to 127.0.0.1:2250" and
// Logger32 showed nothing. Both were true — its UDP sockets speak the WSJT-X
// protocol and discard a bare ADIF record without a word.
func TestWSJTLoggedADIFRoundTrips(t *testing.T) {
	const rec = "<CALL:5>VK9XX<BAND:3>20m<MODE:3>FT8<QSO_DATE:8>20260814<TIME_ON:6>174433<EOR>"
	pkt := BuildWSJTLoggedADIF("OpsLog", rec)

	ev, ok, err := ParseWSJT(pkt)
	if err != nil {
		t.Fatalf("ParseWSJT: %v", err)
	}
	if !ok {
		t.Fatal("our own parser did not recognise the datagram we build")
	}
	if ev.LoggedADIF != rec {
		t.Errorf("ADIF = %q, want %q", ev.LoggedADIF, rec)
	}
	if ev.ProgramID != "OpsLog" {
		t.Errorf("id = %q, want OpsLog — a receiver uses it to tell instances apart", ev.ProgramID)
	}
}

// The header is fixed and other programs match on it byte for byte.
func TestWSJTLoggedADIFHeader(t *testing.T) {
	pkt := BuildWSJTLoggedADIF("OpsLog", "<EOR>")
	want := []byte{
		0xad, 0xbc, 0xcb, 0xda, // magic
		0x00, 0x00, 0x00, 0x02, // schema 2
		0x00, 0x00, 0x00, 0x0c, // type 12 — Logged ADIF
		0x00, 0x00, 0x00, 0x06, // len("OpsLog")
		'O', 'p', 's', 'L', 'o', 'g',
		0x00, 0x00, 0x00, 0x05, // len("<EOR>")
		'<', 'E', 'O', 'R', '>',
	}
	if !bytes.Equal(pkt, want) {
		t.Errorf("packet = % X\nwant     % X", pkt, want)
	}
}

// The julian day has to match Qt's, because the receiver turns it back into a
// date. A day out here files every contact on the wrong day.
func TestJulianDayMatchesKnownValues(t *testing.T) {
	for _, c := range []struct {
		date time.Time
		want int64
	}{
		// Checked against the standard tables: JDN is the count at noon UTC.
		{time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), 2451545},
		{time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC), 2440588},
		{time.Date(2026, 8, 15, 0, 0, 0, 0, time.UTC), 2461268},
	} {
		if got := julianDay(c.date); got != c.want {
			t.Errorf("julianDay(%s) = %d, want %d", c.date.Format("2006-01-02"), got, c.want)
		}
	}
}

// QDateTime is julian day, then milliseconds since midnight, then the time
// spec. A receiver reads them positionally, and a spec of 0 (local time) would
// have it re-interpret a UTC contact in its own zone.
func TestQDateTimeIsUTCAndPositional(t *testing.T) {
	got := buildQDateTimeUTC(time.Date(2026, 8, 15, 17, 44, 33, 0, time.UTC))
	if len(got) != 13 {
		t.Fatalf("%d bytes, want 13", len(got))
	}
	if jd := int64(binary.BigEndian.Uint64(got[0:8])); jd != 2461268 {
		t.Errorf("julian day = %d, want 2461268", jd)
	}
	wantMs := uint32((17*3600 + 44*60 + 33) * 1000)
	if ms := binary.BigEndian.Uint32(got[8:12]); ms != wantMs {
		t.Errorf("ms since midnight = %d, want %d", ms, wantMs)
	}
	if got[12] != 1 {
		t.Errorf("time spec = %d, want 1 (UTC)", got[12])
	}
}

// The QSO Logged message is read positionally: one field out of place shifts
// every one after it, and the receiver silently files nonsense. This pins the
// order against WSJT-X's NetworkMessage.hpp.
func TestWSJTQSOLoggedFieldOrder(t *testing.T) {
	pkt := BuildWSJTQSOLogged("OpsLog", LoggedQSO{
		DXCall: "VK9XX", DXGrid: "QH30", TxFreqHz: 14074000, Mode: "FT8",
		ReportSent: "-12", ReportRcvd: "-08", TxPower: "50", Comments: "tnx",
		Name: "Ken", TimeOn: time.Date(2026, 8, 15, 17, 44, 0, 0, time.UTC),
		TimeOff: time.Date(2026, 8, 15, 17, 45, 0, 0, time.UTC),
		MyCall:  "F4BPO", MyGrid: "IN95", OperatorCall: "F4BPO",
		PropMode: "",
	})
	r := bytes.NewReader(pkt)
	var magic, schema, mtype uint32
	binary.Read(r, binary.BigEndian, &magic)
	binary.Read(r, binary.BigEndian, &schema)
	binary.Read(r, binary.BigEndian, &mtype)
	if magic != wsjtMagic || schema != 2 || mtype != wsjtMsgQSOLogged {
		t.Fatalf("header magic=%08x schema=%d type=%d", magic, schema, mtype)
	}
	id, _ := readQString(r)
	if id != "OpsLog" {
		t.Errorf("id = %q", id)
	}
	r.Seek(13, 1) // QSO off
	call, _ := readQString(r)
	grid, _ := readQString(r)
	var freq uint64
	binary.Read(r, binary.BigEndian, &freq)
	mode, _ := readQString(r)
	sent, _ := readQString(r)
	rcvd, _ := readQString(r)
	if call != "VK9XX" || grid != "QH30" || freq != 14074000 || mode != "FT8" || sent != "-12" || rcvd != "-08" {
		t.Errorf("got call=%q grid=%q freq=%d mode=%q sent=%q rcvd=%q", call, grid, freq, mode, sent, rcvd)
	}
	pwr, _ := readQString(r)
	comments, _ := readQString(r)
	name, _ := readQString(r)
	if pwr != "50" || comments != "tnx" || name != "Ken" {
		t.Errorf("got pwr=%q comments=%q name=%q", pwr, comments, name)
	}
	r.Seek(13, 1) // QSO on
	op, _ := readQString(r)
	myCall, _ := readQString(r)
	myGrid, _ := readQString(r)
	if op != "F4BPO" || myCall != "F4BPO" || myGrid != "IN95" {
		t.Errorf("got op=%q myCall=%q myGrid=%q", op, myCall, myGrid)
	}
}
