package main

import (
	"encoding/binary"
	"math"
	"testing"
)

// ── frame builders (mirror the StreamHub wire format) ───────────────────────

func putf(b []byte, v float64) []byte {
	var t [8]byte
	binary.LittleEndian.PutUint64(t[:], math.Float64bits(v))
	return append(b, t[:]...)
}

func putSignals(b []byte, sigs map[string]points) []byte {
	var n [4]byte
	binary.LittleEndian.PutUint32(n[:], uint32(len(sigs)))
	b = append(b, n[:]...)
	for k, p := range sigs {
		var kl [2]byte
		binary.LittleEndian.PutUint16(kl[:], uint16(len(k)))
		b = append(b, kl[:]...)
		b = append(b, []byte(k)...)
		var cnt [4]byte
		binary.LittleEndian.PutUint32(cnt[:], uint32(len(p.T)))
		b = append(b, cnt[:]...)
		for _, t := range p.T {
			b = putf(b, t)
		}
		for _, v := range p.V {
			b = putf(b, v)
		}
	}
	return b
}

func buildPush(srcID string, sigs map[string]points) []byte {
	b := []byte{1, byte(len(srcID))}
	b = append(b, []byte(srcID)...)
	return putSignals(b, sigs)
}

func buildCapture(trigTime, preSec, postSec float64, sigs map[string]points) []byte {
	b := []byte{2}
	b = putf(b, trigTime)
	b = putf(b, preSec)
	b = putf(b, postSec)
	return putSignals(b, sigs)
}

// ── tests ───────────────────────────────────────────────────────────────────

func TestParsePushRoundTrip(t *testing.T) {
	in := map[string]points{"Sine": {T: []float64{0, 0.1, 0.2}, V: []float64{1, 2, 3}}}
	f, err := parsePush(buildPush("src", in))
	if err != nil {
		t.Fatalf("parsePush: %v", err)
	}
	if f.sourceID != "src" {
		t.Errorf("sourceID = %q, want src", f.sourceID)
	}
	got := f.signals["Sine"]
	if len(got.T) != 3 || got.T[2] != 0.2 || got.V[1] != 2 {
		t.Errorf("bad payload: %+v", got)
	}
}

func TestParsePushRejectsWrongVersion(t *testing.T) {
	if _, err := parsePush([]byte{2, 0, 0}); err == nil {
		t.Error("expected error on non-v1 frame")
	}
}

func TestParsePushTruncated(t *testing.T) {
	full := buildPush("src", map[string]points{"A": {T: []float64{0}, V: []float64{1}}})
	if _, err := parsePush(full[:len(full)-4]); err == nil {
		t.Error("expected truncation error")
	}
}

func TestParseCaptureRoundTrip(t *testing.T) {
	in := map[string]points{"Sig": {T: []float64{1, 2}, V: []float64{9, 8}}}
	f, err := parseCapture(buildCapture(100.5, 0.02, 0.08, in))
	if err != nil {
		t.Fatalf("parseCapture: %v", err)
	}
	if f.trigTime != 100.5 || f.preSec != 0.02 || f.postSec != 0.08 {
		t.Errorf("header = %v/%v/%v", f.trigTime, f.preSec, f.postSec)
	}
	if g := f.signals["Sig"]; len(g.T) != 2 || g.V[0] != 9 {
		t.Errorf("bad capture payload: %+v", g)
	}
}

func TestParseCaptureRejectsWrongVersion(t *testing.T) {
	if _, err := parseCapture([]byte{1, 0, 0, 0}); err == nil {
		t.Error("expected error on non-v2 frame")
	}
}

func TestMergedSortsAndDedups(t *testing.T) {
	c := &client{
		configs: map[string][]signalInfo{},
		zooms:   map[uint32]map[string]points{},
	}
	// two pushes, out of order, with one duplicate timestamp
	c.pushes = []*pushFrame{
		{sourceID: "s", signals: map[string]points{"A": {T: []float64{0.2, 0.1}, V: []float64{2, 1}}}},
		{sourceID: "s", signals: map[string]points{"A": {T: []float64{0.1, 0.3}, V: []float64{1, 3}}}},
	}
	m := c.merged()
	got := m["s:A"]
	want := []float64{0.1, 0.2, 0.3}
	if len(got.T) != len(want) {
		t.Fatalf("len = %d, want %d (%+v)", len(got.T), len(want), got)
	}
	for i := range want {
		if got.T[i] != want[i] {
			t.Errorf("t[%d] = %v, want %v", i, got.T[i], want[i])
		}
	}
}

func TestEdgeCrosses(t *testing.T) {
	rising := points{T: []float64{0, 1, 2}, V: []float64{-1, 1, 2}}
	if !edgeCrosses(rising, 0.5, "rising", 0.0) {
		t.Error("rising cross not detected")
	}
	if edgeCrosses(rising, 0.5, "falling", 0.0) {
		t.Error("false falling detection on rising data")
	}
	falling := points{T: []float64{0, 1}, V: []float64{1, -1}}
	if !edgeCrosses(falling, 0.5, "falling", 0.0) {
		t.Error("falling cross not detected")
	}
	if !edgeCrosses(falling, 0.5, "both", 0.0) {
		t.Error("both should match a falling edge")
	}
}

func TestHas(t *testing.T) {
	if !has("live,zoom,trigger", "zoom") {
		t.Error("has should find zoom")
	}
	if has("live, window", "trigger") {
		t.Error("has should not find trigger")
	}
	if !has("live, window", "window") {
		t.Error("has should trim spaces")
	}
}
