// Copyright 2026. Licensed under Apache-2.0. See LICENSE.
// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.

package device

import (
	"context"
	"encoding/hex"
	"strconv"
	"testing"
	"time"
)

// fakeLink / fakeBackend exercise LiveTransport without real BLE hardware by
// injecting bleBackendFactory.
type fakeLink struct {
	writes map[string][]byte
	reads  map[string][]byte
	closed bool
}

func (l *fakeLink) Write(uuid string, payload []byte) error {
	if l.writes == nil {
		l.writes = map[string][]byte{}
	}
	l.writes[uuid] = payload
	return nil
}
func (l *fakeLink) Read(uuid string) ([]byte, error)                  { return l.reads[uuid], nil }
func (l *fakeLink) Subscribe(uuid string, handler func([]byte)) error { return nil }
func (l *fakeLink) Close() error                                      { l.closed = true; return nil }

type fakeBackend struct {
	link    *fakeLink
	adverts []Advert
}

func (b *fakeBackend) Scan(ctx context.Context, serviceUUIDs []string) ([]Advert, error) {
	return b.adverts, nil
}
func (b *fakeBackend) Connect(ctx context.Context, address string, serviceUUIDs []string) (Link, error) {
	return b.link, nil
}

func withFakeBackend(t *testing.T, be *fakeBackend) {
	t.Helper()
	t.Setenv("PRINTING_PRESS_VERIFY", "") // keep the live path from short-circuiting
	prev := bleBackendFactory
	bleBackendFactory = func() (bleBackend, error) { return be, nil }
	// A fake device has no notify ceremony; suppress any operator-registered
	// telemetrySnapshot so its real-time polling can't stall the transport tests.
	// Snapshot-path tests re-register it after this helper runs.
	prevSnapshot := telemetrySnapshot
	telemetrySnapshot = nil
	t.Cleanup(func() {
		bleBackendFactory = prev
		telemetrySnapshot = prevSnapshot
	})
}

func TestLiveTransportExecuteCommandWritesPayload(t *testing.T) {
	link := &fakeLink{}
	withFakeBackend(t, &fakeBackend{link: link, adverts: []Advert{{Address: "AA:BB:CC:DD:EE:FF", RSSI: -40}}})

	result, err := NewLiveTransport("", 2*time.Second).ExecuteCommand(
		context.Background(),
		CommandDefinition{Name: "probe", CharacteristicUUID: "ff01", PayloadHex: "01ff"},
		nil,
		false,
	)
	if err != nil {
		t.Fatalf("ExecuteCommand: %v", err)
	}
	if result.Transport != "live" {
		t.Errorf("transport = %q, want live", result.Transport)
	}
	want, _ := hex.DecodeString("01ff")
	if got := link.writes["ff01"]; string(got) != string(want) {
		t.Errorf("wrote %x to ff01, want %x", got, want)
	}
	if !link.closed {
		t.Error("link was not closed")
	}
}

func TestLiveTransportDryRunDoesNotWrite(t *testing.T) {
	link := &fakeLink{}
	withFakeBackend(t, &fakeBackend{link: link})

	result, err := NewLiveTransport("AA:BB:CC:DD:EE:FF", 2*time.Second).ExecuteCommand(
		context.Background(),
		CommandDefinition{Name: "probe", CharacteristicUUID: "ff01", PayloadHex: "01ff"},
		nil,
		true,
	)
	if err != nil {
		t.Fatalf("ExecuteCommand: %v", err)
	}
	if !result.DryRun {
		t.Error("expected a dry-run result")
	}
	if len(link.writes) != 0 {
		t.Errorf("dry-run wrote to the device: %v", link.writes)
	}
}

func TestLiveTransportScanSortsByRSSI(t *testing.T) {
	withFakeBackend(t, &fakeBackend{adverts: []Advert{{Address: "weak", RSSI: -80}, {Address: "strong", RSSI: -30}}})

	adverts, err := NewLiveTransport("", 2*time.Second).Scan(context.Background())
	if err != nil {
		t.Fatalf("Scan: %v", err)
	}
	if len(adverts) != 2 || adverts[0].Address != "strong" {
		t.Errorf("scan not sorted strongest-first: %+v", adverts)
	}
}

func TestDialRefusesUnderVerify(t *testing.T) {
	t.Setenv("PRINTING_PRESS_VERIFY", "1")
	if _, err := Dial(context.Background(), "AA:BB:CC:DD:EE:FF", 2*time.Second); err != ErrVerifyMode {
		t.Errorf("Dial under verify = %v, want ErrVerifyMode", err)
	}
}

// testCodec is a minimal DeviceCodec: EncodeCommand turns the first arg into a
// one-byte payload (or returns the static payload when there are no args), and
// DecodeTelemetry returns the first raw byte as an int.
type testCodec struct{}

func (testCodec) EncodeCommand(command CommandDefinition, args []string) ([]byte, error) {
	if len(args) == 0 {
		return hex.DecodeString(command.PayloadHex)
	}
	n, err := strconv.Atoi(args[0])
	if err != nil {
		return nil, err
	}
	return []byte{byte(n)}, nil
}

func (testCodec) DecodeTelemetry(field StatusField, raw []byte) (any, error) {
	if len(raw) == 0 {
		return nil, nil
	}
	return int(raw[0]), nil
}

func TestLiveTransportStatusDecodesWithCodec(t *testing.T) {
	if len(StatusFields) == 0 {
		t.Skip("device has no telemetry fields")
	}
	uuid := StatusFields[0].SourceCharacteristicUUID
	link := &fakeLink{reads: map[string][]byte{uuid: {0x2a}}}
	withFakeBackend(t, &fakeBackend{link: link, adverts: []Advert{{Address: "AA:BB:CC:DD:EE:FF", RSSI: -30}}})
	prev := codec
	codec = testCodec{}
	t.Cleanup(func() { codec = prev })

	snap, err := NewLiveTransport("", 2*time.Second).Status(context.Background())
	if err != nil {
		t.Fatalf("Status: %v", err)
	}
	entry, _ := snap.Telemetry[StatusFields[0].Name].(map[string]any)
	if entry["value"] != 42 {
		t.Errorf("decoded telemetry value = %v, want 42", entry["value"])
	}
}

func TestStatusUsesTelemetrySnapshotForNotifyOnly(t *testing.T) {
	if len(StatusFields) == 0 {
		t.Skip("device has no telemetry fields")
	}
	uuid := StatusFields[0].SourceCharacteristicUUID
	// A GATT read would yield 0x2a (42); the snapshot yields 0x07 (7). Asserting 7
	// proves Status decoded the snapshot frame, not a characteristic read.
	link := &fakeLink{reads: map[string][]byte{uuid: {0x2a}}}
	withFakeBackend(t, &fakeBackend{link: link, adverts: []Advert{{Address: "AA:BB:CC:DD:EE:FF", RSSI: -30}}})
	prevCodec := codec
	codec = testCodec{}
	t.Cleanup(func() { codec = prevCodec })
	// withFakeBackend nulled telemetrySnapshot and restores it on cleanup; install ours.
	telemetrySnapshot = func(ctx context.Context, l Link) ([]byte, error) { return []byte{0x07}, nil }

	snap, err := NewLiveTransport("", 2*time.Second).Status(context.Background())
	if err != nil {
		t.Fatalf("Status: %v", err)
	}
	entry, _ := snap.Telemetry[StatusFields[0].Name].(map[string]any)
	if entry["value"] != 7 {
		t.Errorf("decoded value = %v, want 7 (from snapshot frame, not the GATT read)", entry["value"])
	}
	if entry["raw_hex"] != "07" {
		t.Errorf("raw_hex = %v, want 07 (snapshot frame)", entry["raw_hex"])
	}
}

func TestStatusSnapshotErrorPropagates(t *testing.T) {
	withFakeBackend(t, &fakeBackend{link: &fakeLink{}, adverts: []Advert{{Address: "AA:BB:CC:DD:EE:FF", RSSI: -30}}})
	// withFakeBackend nulled telemetrySnapshot and restores it on cleanup; install ours.
	telemetrySnapshot = func(ctx context.Context, l Link) ([]byte, error) { return nil, context.DeadlineExceeded }

	if _, err := NewLiveTransport("", 2*time.Second).Status(context.Background()); err == nil {
		t.Error("Status should surface a telemetrySnapshot error")
	}
}

func TestLiveTransportEncodesParameterizedCommandWithCodec(t *testing.T) {
	link := &fakeLink{}
	withFakeBackend(t, &fakeBackend{link: link, adverts: []Advert{{Address: "AA:BB:CC:DD:EE:FF", RSSI: -30}}})
	prev := codec
	codec = testCodec{}
	t.Cleanup(func() { codec = prev })

	result, err := NewLiveTransport("", 2*time.Second).ExecuteCommand(
		context.Background(),
		CommandDefinition{Name: "set-level", CharacteristicUUID: "ff01", Parameters: []string{"level"}},
		[]string{"7"},
		false,
	)
	if err != nil {
		t.Fatalf("ExecuteCommand: %v", err)
	}
	if got := link.writes["ff01"]; len(got) != 1 || got[0] != 7 {
		t.Errorf("codec-encoded write = %x, want 07", got)
	}
	if result.PayloadHex != "07" {
		t.Errorf("result payload = %q, want 07", result.PayloadHex)
	}
}

func TestLiveTransportParameterizedCommandRequiresCodec(t *testing.T) {
	withFakeBackend(t, &fakeBackend{link: &fakeLink{}, adverts: []Advert{{Address: "AA:BB:CC:DD:EE:FF", RSSI: -30}}})
	prev := codec
	codec = nil
	t.Cleanup(func() { codec = prev })

	_, err := NewLiveTransport("", 2*time.Second).ExecuteCommand(
		context.Background(),
		CommandDefinition{Name: "set-level", CharacteristicUUID: "ff01", Parameters: []string{"level"}},
		[]string{"7"},
		false,
	)
	if err == nil {
		t.Error("parameterized command with no codec should error, not write a static payload")
	}
}
