package greencell

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

	"drivervault/apiserver/internal/plugins"
)

// ---- descriptor & registration -----------------------------------------------

func TestDescriptor(t *testing.T) {
	d := (&Plugin{}).Descriptor()
	if d.Name != "greencell" {
		t.Fatalf("name = %q, want greencell", d.Name)
	}
	if d.Kind != plugins.KindBuiltin {
		t.Fatalf("kind = %q, want builtin", d.Kind)
	}
	if len(d.Capabilities) == 0 {
		t.Fatal("expected capabilities")
	}

	fields := map[string]plugins.ConfigField{}
	for _, f := range d.ConfigFields {
		fields[f.Key] = f
	}
	for _, k := range []string{"host", "port", "tls", "username", "password", "serial", "commandTopic", "timeout"} {
		if _, ok := fields[k]; !ok {
			t.Errorf("config field %q should be present", k)
		}
	}
	// Nothing is required at the global layer — the per-user cascade may supply
	// the broker instead, exactly as it does for the other connectors.
	for k, f := range fields {
		if f.Required {
			t.Errorf("config field %q must not be required at the global layer", k)
		}
	}
	if !fields["password"].Secret {
		t.Error("the broker password must be marked secret")
	}
	if fields["username"].Secret {
		t.Error("the broker username is not a secret")
	}
	if fields["tls"].Type != "select" {
		t.Errorf("tls type = %q, want select", fields["tls"].Type)
	}
	if fields["port"].Default != defaultPort {
		t.Errorf("port default = %q, want %q", fields["port"].Default, defaultPort)
	}
}

func TestRegistered(t *testing.T) {
	// init() must have registered the factory; registering twice panics, so a
	// duplicate name would surface at startup rather than here.
	defer func() {
		if r := recover(); r == nil {
			t.Fatal("expected a panic on duplicate registration, meaning the plugin registered itself")
		}
	}()
	plugins.Register("greencell", func() plugins.Plugin { return &Plugin{} })
}

// ---- config ------------------------------------------------------------------

func TestInitBuildsBrokerAddress(t *testing.T) {
	cases := []struct {
		name    string
		config  map[string]string
		address string
		tls     bool
	}{
		{"explicit port", map[string]string{"host": "10.2.1.10", "port": "1884"}, "10.2.1.10:1884", false},
		{"default plain port", map[string]string{"host": "mqtt.local"}, "mqtt.local:1883", false},
		{"default tls port", map[string]string{"host": "mqtt.local", "tls": "on"}, "mqtt.local:8883", true},
		{"tls with explicit port", map[string]string{"host": "mqtt.local", "tls": "true", "port": "9001"}, "mqtt.local:9001", true},
		{"ipv6 host", map[string]string{"host": "fd00::1", "port": "1883"}, "[fd00::1]:1883", false},
		{"no host", map[string]string{}, "", false},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			var p Plugin
			if err := p.Init(context.Background(), tc.config); err != nil {
				t.Fatalf("Init: %v", err)
			}
			if p.address != tc.address {
				t.Errorf("address = %q, want %q", p.address, tc.address)
			}
			if p.useTLS != tc.tls {
				t.Errorf("useTLS = %v, want %v", p.useTLS, tc.tls)
			}
		})
	}
}

func TestSnapshotWithoutBrokerIsAnError(t *testing.T) {
	var p Plugin
	_ = p.Init(context.Background(), map[string]string{})
	if _, _, err := p.snapshot(); err == nil {
		t.Fatal("expected an error when no broker is configured")
	}
}

func TestParseTimeout(t *testing.T) {
	cases := map[string]time.Duration{
		"":       defaultTimeout,
		"abc":    defaultTimeout,
		"0":      defaultTimeout,
		"-5":     defaultTimeout,
		" 20 ":   20 * time.Second,
		"1":      minTimeout,
		"600":    maxTimeout, // clamped
		"999999": maxTimeout,
	}
	for in, want := range cases {
		if got := parseTimeout(in); got != want {
			t.Errorf("parseTimeout(%q) = %v, want %v", in, got, want)
		}
	}
}

func TestTruthy(t *testing.T) {
	for _, v := range []string{"on", "ON", "true", "yes", "1", " tls "} {
		if !truthy(v) {
			t.Errorf("truthy(%q) = false", v)
		}
	}
	for _, v := range []string{"", "off", "false", "no", "0", "maybe"} {
		if truthy(v) {
			t.Errorf("truthy(%q) = true", v)
		}
	}
}

// ---- serials & announcements -------------------------------------------------

func TestDeviceModel(t *testing.T) {
	// The HabuDen pattern comes from greencell_client's GreencellUtils.
	if got := deviceModel("EVGC021B22752405ZM0018"); got != nameHabuDen {
		t.Errorf("HabuDen serial named %q, want %q", got, nameHabuDen)
	}
	for _, sn := range []string{"EVGC031B22752405ZM0018", "EVGC022B22752405ZM0018", "HABU_DEN", ""} {
		if got := deviceModel(sn); got != nameGeneric {
			t.Errorf("deviceModel(%q) = %q, want %q", sn, got, nameGeneric)
		}
	}
}

func TestParseAnnouncement(t *testing.T) {
	dev, ok := parseAnnouncement([]byte(`{"id":"EVGC021B22752405ZM0018","fw":"1.2.3"}`))
	if !ok {
		t.Fatal("a payload with an id should parse")
	}
	if dev.SN != "EVGC021B22752405ZM0018" {
		t.Errorf("sn = %q", dev.SN)
	}
	if dev.Model != nameHabuDen || !strings.HasPrefix(dev.Name, nameHabuDen+" ") {
		t.Errorf("name/model = %q / %q", dev.Name, dev.Model)
	}
	// Fields this connector has no schema for must survive.
	if !strings.Contains(string(dev.Announcement), `"fw":"1.2.3"`) {
		t.Errorf("announcement should be passed through verbatim, got %s", dev.Announcement)
	}

	for _, bad := range []string{`{"id":""}`, `{"id":"   "}`, `{"name":"BROADCAST"}`, `not json`, ``} {
		if _, ok := parseAnnouncement([]byte(bad)); ok {
			t.Errorf("parseAnnouncement(%q) should have been rejected", bad)
		}
	}
}

func TestTopicSuffix(t *testing.T) {
	sn := "SN1"
	for _, want := range telemetryTopics {
		got, ok := topicSuffix(evsePrefix+sn+"/"+want, sn)
		if !ok || got != want {
			t.Errorf("topicSuffix for %q = %q/%v", want, got, ok)
		}
	}
	for _, topic := range []string{
		evsePrefix + "OTHER/current", // another charger
		evsePrefix + sn + "/unknown", // a topic we do not model
		discoveryTopic,
		evsePrefix + sn, // no suffix at all
	} {
		if _, ok := topicSuffix(topic, sn); ok {
			t.Errorf("topicSuffix(%q) should not match", topic)
		}
	}
}

// ---- payload parsing ---------------------------------------------------------

func TestAsNumber(t *testing.T) {
	f := asNumber(json.RawMessage(`230.5`))
	if f == nil || *f != 230.5 {
		t.Errorf("number = %v", f)
	}
	// Some firmware quotes its numbers.
	f = asNumber(json.RawMessage(`"16000"`))
	if f == nil || *f != 16000 {
		t.Errorf("quoted number = %v", f)
	}
	for _, bad := range []string{`null`, `"abc"`, `{}`, ``} {
		if got := asNumber(json.RawMessage(bad)); got != nil {
			t.Errorf("asNumber(%q) = %v, want nil", bad, *got)
		}
	}
}

func TestParsePhasesLeavesMissingPhasesUnset(t *testing.T) {
	// A single-phase charge reports only l1; l2/l3 must stay nil rather than
	// reading as a real zero.
	p := parsePhases(json.RawMessage(`{"l1":16000}`))
	if p.L1 == nil || *p.L1 != 16000 {
		t.Errorf("l1 = %v", p.L1)
	}
	if p.L2 != nil || p.L3 != nil {
		t.Errorf("missing phases should be nil, got %v %v", p.L2, p.L3)
	}
	if got := parsePhases(json.RawMessage(`not json`)); got.L1 != nil {
		t.Error("a malformed payload should yield no phases")
	}
	if got := parsePhases(nil); got.L1 != nil {
		t.Error("a missing payload should yield no phases")
	}
}

func TestNormalizeState(t *testing.T) {
	if got := normalizeState("CHARGING"); got != stateCharging {
		t.Errorf("CHARGING -> %q", got)
	}
	if got := normalizeState(" waiting_for_car "); got != stateWaitingForCar {
		t.Errorf("waiting_for_car -> %q", got)
	}
	for _, in := range []string{"", "SOMETHING_NEW", "UNKNOWN"} {
		if got := normalizeState(in); got != stateUnknown {
			t.Errorf("normalizeState(%q) = %q, want unknown", in, got)
		}
	}
}

func TestNormalizeAccess(t *testing.T) {
	cases := map[string]string{
		"EXECUTE": accessExecute, "read": accessRead, "DISABLED": accessDisabled,
		"UNAVAILABLE": accessUnavailable,
		// OFFLINE is greencell_client's deprecated spelling of UNAVAILABLE.
		"OFFLINE": accessUnavailable,
		"":        accessUnknown, "nonsense": accessUnknown,
	}
	for in, want := range cases {
		if got := normalizeAccess(in); got != want {
			t.Errorf("normalizeAccess(%q) = %q, want %q", in, got, want)
		}
	}
}

func TestBuildStateThreePhaseCharge(t *testing.T) {
	raw := map[string]json.RawMessage{
		topicCurrent:     json.RawMessage(`{"l1":16000,"l2":15980,"l3":16010}`),
		topicVoltage:     json.RawMessage(`{"l1":230.1,"l2":229.7,"l3":231.0}`),
		topicPower:       json.RawMessage(`{"momentary":11040}`),
		topicStatus:      json.RawMessage(`{"state":"CHARGING"}`),
		topicDeviceState: json.RawMessage(`{"level":"EXECUTE"}`),
	}
	st := buildState("EVGC021B22752405ZM0018", raw)

	if !st.Complete {
		t.Error("all five topics reported, so the reading is complete")
	}
	// Current is published in milliamps.
	if st.CurrentA.L1 == nil || *st.CurrentA.L1 != 16 {
		t.Errorf("l1 current = %v A, want 16", st.CurrentA.L1)
	}
	if st.VoltageV.L1 == nil || *st.VoltageV.L1 != 230.1 {
		t.Errorf("l1 voltage = %v", st.VoltageV.L1)
	}
	if st.PowerW == nil || *st.PowerW != 11040 {
		t.Errorf("power = %v", st.PowerW)
	}
	if st.Status != stateCharging || !st.Charging || !st.Plugged || st.Fault {
		t.Errorf("status flags: %q charging=%v plugged=%v fault=%v", st.Status, st.Charging, st.Plugged, st.Fault)
	}
	if st.AccessLevel != accessExecute || !st.CanExecute || !st.Available {
		t.Errorf("access: %q canExecute=%v available=%v", st.AccessLevel, st.CanExecute, st.Available)
	}
	if st.LivePhases != 3 {
		t.Errorf("livePhases = %d, want 3", st.LivePhases)
	}
	if st.Model != nameHabuDen {
		t.Errorf("model = %q", st.Model)
	}
	if st.ObservedAt.IsZero() {
		t.Error("observedAt should be stamped")
	}
}

func TestBuildStateSinglePhaseAndPartial(t *testing.T) {
	// Only two topics reported: the rest must read as unset, not as zero.
	raw := map[string]json.RawMessage{
		topicCurrent: json.RawMessage(`{"l1":16000,"l2":30,"l3":0}`),
		topicStatus:  json.RawMessage(`{"state":"CHARGING"}`),
	}
	st := buildState("SN1", raw)

	if st.Complete {
		t.Error("a partial reading must not claim to be complete")
	}
	if st.Received[topicCurrent] != true || st.Received[topicPower] != false {
		t.Errorf("received map = %v", st.Received)
	}
	if st.PowerW != nil {
		t.Errorf("power = %v, want nil when the topic was silent", *st.PowerW)
	}
	// 30 mA is 0.03 A — noise on an idle phase, not a live one.
	if st.LivePhases != 1 {
		t.Errorf("livePhases = %d, want 1", st.LivePhases)
	}
	if st.AccessLevel != accessUnknown {
		t.Errorf("access level = %q, want unknown when device_state was silent", st.AccessLevel)
	}
	if st.Model != nameGeneric {
		t.Errorf("model = %q, want the generic name for a non-HabuDen serial", st.Model)
	}
}

func TestBuildStateUnavailable(t *testing.T) {
	// The device signals unavailability through the status topic itself, which
	// upstream detects by substring because the payload is not a state document.
	for _, payload := range []string{`{"state":"UNAVAILABLE"}`, `"OFFLINE"`, `{"status":"UNAVAILABLE"}`} {
		st := buildState("SN1", map[string]json.RawMessage{
			topicStatus:      json.RawMessage(payload),
			topicDeviceState: json.RawMessage(`{"level":"READ"}`),
		})
		if st.Status != stateUnavailable {
			t.Errorf("payload %s -> status %q, want unavailable", payload, st.Status)
		}
		if st.Available {
			t.Errorf("payload %s should not read as available", payload)
		}
	}
}

func TestBuildStateErrorStates(t *testing.T) {
	for _, tc := range []struct {
		state   string
		fault   bool
		plugged bool
	}{
		{"ERROR_CAR", true, true},
		{"ERROR_EVSE", true, false},
		{"IDLE", false, false},
		{"FINISHED", false, true},
	} {
		st := buildState("SN1", map[string]json.RawMessage{
			topicStatus: json.RawMessage(`{"state":"` + tc.state + `"}`),
		})
		if st.Fault != tc.fault || st.Plugged != tc.plugged {
			t.Errorf("%s: fault=%v plugged=%v, want %v/%v", tc.state, st.Fault, st.Plugged, tc.fault, tc.plugged)
		}
	}
}

func TestBuildStateDisabledIsNotAvailable(t *testing.T) {
	st := buildState("SN1", map[string]json.RawMessage{
		topicStatus:      json.RawMessage(`{"state":"IDLE"}`),
		topicDeviceState: json.RawMessage(`{"level":"DISABLED"}`),
	})
	if st.Available || st.CanExecute {
		t.Errorf("a DISABLED device is neither available nor executable: %+v", st)
	}
}

// ---- Invoke dispatch ---------------------------------------------------------

func TestInvokeUnknownAction(t *testing.T) {
	var p Plugin
	_ = p.Init(context.Background(), map[string]string{"host": "127.0.0.1"})
	if _, err := p.Invoke(context.Background(), "nope", nil); err == nil {
		t.Fatal("expected an error for an unknown action")
	}
}

func TestInvokeChargerStateRequiresSerial(t *testing.T) {
	var p Plugin
	_ = p.Init(context.Background(), map[string]string{"host": "127.0.0.1"})
	_, err := p.Invoke(context.Background(), "charger-state", nil)
	if err == nil || !strings.Contains(err.Error(), "serial") {
		t.Fatalf("error = %v, want a missing-serial error", err)
	}
}

func TestInvokeRejectsMalformedParams(t *testing.T) {
	var p Plugin
	_ = p.Init(context.Background(), map[string]string{"host": "127.0.0.1"})
	if _, err := p.Invoke(context.Background(), "chargers", json.RawMessage(`[`)); err == nil {
		t.Fatal("expected an error for malformed params")
	}
}

// ---- end to end against a broker ---------------------------------------------

func TestDiscoverCollectsEveryDeviceThatAnswers(t *testing.T) {
	addr := startBroker(t, []brokerMsg{
		{discoveryTopic, `{"id":"EVGC021B22752405ZM0018"}`},
		{discoveryTopic, `{"id":"SOMETHINGELSE"}`},
		{discoveryTopic, `{"id":"EVGC021B22752405ZM0018"}`}, // a repeat is one device
		{discoveryTopic, `{"name":"BROADCAST"}`},            // our own echo, not a device
	})

	p := pluginAt(t, addr, map[string]string{"timeout": "5"})
	devices, err := p.discover(context.Background())
	if err != nil {
		t.Fatalf("discover: %v", err)
	}
	if len(devices) != 2 {
		t.Fatalf("found %d devices, want 2: %+v", len(devices), devices)
	}
	if devices[0].SN != "EVGC021B22752405ZM0018" || devices[0].Model != nameHabuDen {
		t.Errorf("first device = %+v", devices[0])
	}
	if devices[1].Model != nameGeneric {
		t.Errorf("second device model = %q, want the generic name", devices[1].Model)
	}
}

func TestDiscoverReturnsEmptyOnSilence(t *testing.T) {
	addr := startBroker(t, nil)
	p := pluginAt(t, addr, map[string]string{"timeout": "1"})
	devices, err := p.discover(context.Background())
	if err != nil {
		t.Fatalf("discover: %v", err)
	}
	if len(devices) != 0 {
		t.Fatalf("want no devices, got %+v", devices)
	}
}

func TestChargerStateReadsTelemetry(t *testing.T) {
	sn := "EVGC021B22752405ZM0018"
	addr := startBroker(t, []brokerMsg{
		{evsePrefix + sn + "/current", `{"l1":16000,"l2":0,"l3":0}`},
		{evsePrefix + sn + "/voltage", `{"l1":230,"l2":231,"l3":229}`},
		{evsePrefix + sn + "/power", `{"momentary":3680}`},
		{evsePrefix + sn + "/status", `{"state":"CHARGING"}`},
		{evsePrefix + sn + "/device_state", `{"level":"READ"}`},
		// Another charger on the same broker must not bleed into this reading.
		{evsePrefix + "OTHER/power", `{"momentary":99999}`},
	})

	p := pluginAt(t, addr, map[string]string{"timeout": "5"})
	st, err := p.chargerState(context.Background(), sn)
	if err != nil {
		t.Fatalf("chargerState: %v", err)
	}
	if !st.Complete {
		t.Errorf("expected a complete reading, got %v", st.Received)
	}
	if st.PowerW == nil || *st.PowerW != 3680 {
		t.Errorf("power = %v, want 3680 (the other charger's reading must not leak in)", st.PowerW)
	}
	if st.CurrentA.L1 == nil || *st.CurrentA.L1 != 16 {
		t.Errorf("l1 = %v A, want 16", st.CurrentA.L1)
	}
	if st.LivePhases != 1 {
		t.Errorf("livePhases = %d, want 1", st.LivePhases)
	}
	if st.AccessLevel != accessRead || st.CanExecute {
		t.Errorf("READ mode should not report as executable: %+v", st)
	}
}

func TestChargerStateReturnsPartialAtTheDeadline(t *testing.T) {
	sn := "SN1"
	addr := startBroker(t, []brokerMsg{
		{evsePrefix + sn + "/status", `{"state":"IDLE"}`},
	})

	p := pluginAt(t, addr, map[string]string{"timeout": "1"})
	st, err := p.chargerState(context.Background(), sn)
	if err != nil {
		t.Fatalf("a partial reading should not be an error: %v", err)
	}
	if st.Complete || st.Status != stateIdle {
		t.Errorf("state = %+v", st)
	}
	if st.Received[topicVoltage] {
		t.Error("voltage was never published, so it must read as not received")
	}
}

func TestChargerStateErrorsOnSilence(t *testing.T) {
	addr := startBroker(t, nil)
	p := pluginAt(t, addr, map[string]string{"timeout": "1"})
	_, err := p.chargerState(context.Background(), "SN1")
	if err == nil || !strings.Contains(err.Error(), "no data from charger") {
		t.Fatalf("error = %v, want a no-data error", err)
	}
}

func TestChargerStateFailsWithoutABroker(t *testing.T) {
	// Nothing is listening, so the session cannot be opened at all.
	p := pluginAt(t, "127.0.0.1:1", map[string]string{"timeout": "1"})
	if _, err := p.chargerState(context.Background(), "SN1"); err == nil {
		t.Fatal("expected a connection error")
	}
}

func TestHealthCheckClassifiesTheThreeOutcomes(t *testing.T) {
	t.Run("ok", func(t *testing.T) {
		addr := startBroker(t, []brokerMsg{{discoveryTopic, `{"id":"EVGC021B22752405ZM0018"}`}})
		p := pluginAt(t, addr, map[string]string{"timeout": "5"})
		h := p.HealthCheck(context.Background())
		if h.Status != plugins.StatusOK {
			t.Fatalf("status = %q (%s), want ok", h.Status, h.Detail)
		}
		if !strings.Contains(h.Detail, "1 Greencell device") {
			t.Errorf("detail = %q", h.Detail)
		}
	})

	t.Run("degraded when the broker answers but no charger does", func(t *testing.T) {
		addr := startBroker(t, nil)
		p := pluginAt(t, addr, map[string]string{"timeout": "1"})
		h := p.HealthCheck(context.Background())
		if h.Status != plugins.StatusDegraded {
			t.Fatalf("status = %q (%s), want degraded", h.Status, h.Detail)
		}
	})

	t.Run("down when the broker is unreachable", func(t *testing.T) {
		p := pluginAt(t, "127.0.0.1:1", map[string]string{"timeout": "1"})
		h := p.HealthCheck(context.Background())
		if h.Status != plugins.StatusDown {
			t.Fatalf("status = %q (%s), want down", h.Status, h.Detail)
		}
	})
}

func TestInvokeChargersShape(t *testing.T) {
	addr := startBroker(t, []brokerMsg{{discoveryTopic, `{"id":"EVGC021B22752405ZM0018"}`}})
	p := pluginAt(t, addr, map[string]string{"timeout": "5"})

	raw, err := p.Invoke(context.Background(), "chargers", nil)
	if err != nil {
		t.Fatalf("Invoke: %v", err)
	}
	var doc struct {
		Chargers []Device `json:"chargers"`
	}
	if err := json.Unmarshal(raw, &doc); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if len(doc.Chargers) != 1 || doc.Chargers[0].SN != "EVGC021B22752405ZM0018" {
		t.Fatalf("chargers = %+v", doc.Chargers)
	}
}

func TestInvokeChargerStateFallsBackToTheConfiguredSerial(t *testing.T) {
	sn := "EVGC021B22752405ZM0018"
	addr := startBroker(t, []brokerMsg{{evsePrefix + sn + "/status", `{"state":"FINISHED"}`}})
	p := pluginAt(t, addr, map[string]string{"timeout": "1", "serial": sn})

	raw, err := p.Invoke(context.Background(), "charger-state", nil)
	if err != nil {
		t.Fatalf("Invoke: %v", err)
	}
	var st State
	if err := json.Unmarshal(raw, &st); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if st.SN != sn || st.Status != stateFinished {
		t.Fatalf("state = %+v", st)
	}
}

// ---- test broker -------------------------------------------------------------

// brokerMsg is one publication the test broker pushes once the plugin has
// subscribed and sent its discovery broadcast.
type brokerMsg struct {
	topic   string
	payload string
}

// pluginAt returns a plugin configured to talk to addr.
func pluginAt(t *testing.T, addr string, extra map[string]string) *Plugin {
	t.Helper()
	host, port, err := net.SplitHostPort(addr)
	if err != nil {
		t.Fatalf("bad address %q: %v", addr, err)
	}
	config := map[string]string{"host": host, "port": port}
	for k, v := range extra {
		config[k] = v
	}
	p := &Plugin{}
	if err := p.Init(context.Background(), config); err != nil {
		t.Fatalf("Init: %v", err)
	}
	return p
}

// startBroker runs an MQTT 3.1.1 broker that does just enough for these tests:
// accept a connection, acknowledge the subscription, and — once the client sends
// its discovery broadcast — push the scripted messages back. It speaks the wire
// format directly rather than reusing internal/mqtt, so a bug in the client
// cannot hide behind a matching bug in the fixture.
func startBroker(t *testing.T, script []brokerMsg) string {
	t.Helper()
	return startBrokerRecording(t, nil, script)
}

// startBrokerRecording is startBroker with a channel that receives everything the
// client publishes, so a test can assert on what went out as well as what came
// back. A nil channel records nothing.
func startBrokerRecording(t *testing.T, seen chan<- brokerMsg, script []brokerMsg) string {
	t.Helper()
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen: %v", err)
	}
	var wg sync.WaitGroup
	// Close the listener before waiting: the accept loop only ends when the
	// listener does, and cleanups run last-registered first.
	t.Cleanup(func() { _ = ln.Close(); wg.Wait() })
	wg.Add(1)
	go func() {
		defer wg.Done()
		for {
			conn, err := ln.Accept()
			if err != nil {
				return
			}
			wg.Add(1)
			go func() {
				defer wg.Done()
				defer conn.Close()
				serveBroker(conn, seen, script)
			}()
		}
	}()
	return ln.Addr().String()
}

func serveBroker(conn net.Conn, seen chan<- brokerMsg, script []brokerMsg) {
	br := bufio.NewReader(conn)
	for {
		typ, _, body, err := readRaw(br)
		if err != nil {
			return
		}
		switch typ {
		case 1: // CONNECT
			if _, err := conn.Write(frame(2, 0, []byte{0, 0})); err != nil {
				return
			}
		case 8: // SUBSCRIBE — acknowledge every requested filter with QoS 0
			if len(body) < 2 {
				return
			}
			ack := append([]byte{body[0], body[1]}, make([]byte, countFilters(body[2:]))...)
			if _, err := conn.Write(frame(9, 0, ack)); err != nil {
				return
			}
		case 3: // PUBLISH — from the client; record it, then answer with the script
			if seen != nil {
				if topic, rest, err := splitRawString(body); err == nil {
					select {
					case seen <- brokerMsg{topic, string(rest)}:
					default:
					}
				}
			}
			for _, m := range script {
				pub := append(encRawString(m.topic), m.payload...)
				if _, err := conn.Write(frame(3, 0, pub)); err != nil {
					return
				}
			}
		case 12: // PINGREQ
			if _, err := conn.Write(frame(13, 0, nil)); err != nil {
				return
			}
		case 14: // DISCONNECT
			return
		}
	}
}

// countFilters counts the topic filters in a SUBSCRIBE payload so the SUBACK can
// carry one return code per filter.
func countFilters(b []byte) int {
	n := 0
	for len(b) >= 3 {
		l := int(binary.BigEndian.Uint16(b))
		if len(b) < 2+l+1 {
			return n
		}
		b = b[2+l+1:]
		n++
	}
	return n
}

// splitRawString peels an MQTT UTF-8 string off the front of b.
func splitRawString(b []byte) (string, []byte, error) {
	if len(b) < 2 {
		return "", nil, io.ErrUnexpectedEOF
	}
	n := int(binary.BigEndian.Uint16(b))
	if len(b) < 2+n {
		return "", nil, io.ErrUnexpectedEOF
	}
	return string(b[2 : 2+n]), b[2+n:], nil
}

func encRawString(s string) []byte {
	out := binary.BigEndian.AppendUint16(nil, uint16(len(s)))
	return append(out, s...)
}

func frame(typ, flags byte, body []byte) []byte {
	out := []byte{typ<<4 | flags}
	n := len(body)
	for {
		digit := byte(n % 128)
		n /= 128
		if n > 0 {
			digit |= 0x80
		}
		out = append(out, digit)
		if n == 0 {
			break
		}
	}
	return append(out, body...)
}

func readRaw(br *bufio.Reader) (typ, flags byte, body []byte, err error) {
	head, err := br.ReadByte()
	if err != nil {
		return 0, 0, nil, err
	}
	var (
		length     int
		multiplier = 1
	)
	for i := 0; i < 4; i++ {
		digit, err := br.ReadByte()
		if err != nil {
			return 0, 0, nil, err
		}
		length += int(digit&0x7F) * multiplier
		if digit&0x80 == 0 {
			break
		}
		multiplier *= 128
	}
	body = make([]byte, length)
	if _, err := io.ReadFull(br, body); err != nil {
		return 0, 0, nil, err
	}
	return head >> 4, head & 0x0F, body, nil
}

// ---- the operator-supplied QUERY topic ---------------------------------------

func TestCommandTopicFor(t *testing.T) {
	var p Plugin
	_ = p.Init(context.Background(), map[string]string{
		"host": "h", "commandTopic": "/greencell/evse/" + snPlaceholder + "/command",
	})
	if got := p.commandTopicFor("SN1"); got != "/greencell/evse/SN1/command" {
		t.Errorf("commandTopicFor = %q", got)
	}

	// A topic without the placeholder is used as given.
	_ = p.Init(context.Background(), map[string]string{"host": "h", "commandTopic": "/fixed/topic"})
	if got := p.commandTopicFor("SN1"); got != "/fixed/topic" {
		t.Errorf("commandTopicFor = %q, want the literal topic", got)
	}

	// Unset means listen-only; nothing is ever published.
	_ = p.Init(context.Background(), map[string]string{"host": "h"})
	if got := p.commandTopicFor("SN1"); got != "" {
		t.Errorf("commandTopicFor = %q, want empty when unconfigured", got)
	}
}

func TestChargerStateSendsQueryWhenATopicIsConfigured(t *testing.T) {
	sn := "SN1"
	seen := make(chan brokerMsg, 4)
	addr := startBrokerRecording(t, seen, []brokerMsg{
		{evsePrefix + sn + "/status", `{"state":"IDLE"}`},
	})

	p := pluginAt(t, addr, map[string]string{
		"timeout": "1", "commandTopic": "/greencell/evse/" + snPlaceholder + "/command",
	})
	if _, err := p.chargerState(context.Background(), sn); err != nil {
		t.Fatalf("chargerState: %v", err)
	}

	var got []brokerMsg
	close(seen)
	for m := range seen {
		got = append(got, m)
	}
	// The discovery broadcast, then the QUERY on the configured topic.
	var query *brokerMsg
	for i := range got {
		if got[i].topic == "/greencell/evse/SN1/command" {
			query = &got[i]
		}
	}
	if query == nil {
		t.Fatalf("no QUERY was published; broker saw %+v", got)
	}
	if query.payload != queryCommand {
		t.Errorf("QUERY payload = %q, want %q", query.payload, queryCommand)
	}
}

func TestChargerStateSendsNothingWithoutACommandTopic(t *testing.T) {
	sn := "SN1"
	seen := make(chan brokerMsg, 4)
	addr := startBrokerRecording(t, seen, []brokerMsg{
		{evsePrefix + sn + "/status", `{"state":"IDLE"}`},
	})

	p := pluginAt(t, addr, map[string]string{"timeout": "1"})
	if _, err := p.chargerState(context.Background(), sn); err != nil {
		t.Fatalf("chargerState: %v", err)
	}

	close(seen)
	for m := range seen {
		if m.topic != broadcastTopic {
			t.Errorf("unconfigured, the plugin must publish only the discovery broadcast; it also sent %+v", m)
		}
	}
}
