package api

import (
	"testing"

	pb "github.com/antitbone/dwarfctl/proto"
	"google.golang.org/protobuf/proto"
)

// TestModuleIDForCmd verifies that every command range maps to the correct
// module_id, matching WsCmd.getModuleId() from the original APK.
func TestModuleIDForCmd(t *testing.T) {
	cases := []struct {
		name     string
		cmd      uint32
		wantMod  uint32
	}{
		// Camera Tele (10000–10499)
		{"tele open camera", CmdTeleOpenCamera, ModuleCameraTele},
		{"tele photo", CmdTelePhotograph, ModuleCameraTele},
		{"tele get all params", CmdTeleGetAllParams, ModuleCameraTele},
		{"tele boundary low", 10000, ModuleCameraTele},
		{"tele boundary high-1", 10499, ModuleCameraTele},

		// Astro (11000–11499)
		{"astro calibrate", CmdAstroStartCalibration, ModuleAstro},
		{"astro goto dso", CmdAstroStartGotoDSO, ModuleAstro},
		{"astro boundary low", 11000, ModuleAstro},
		{"astro boundary high-1", 11499, ModuleAstro},

		// Camera Wide (12000–12499)
		{"wide open camera", CmdWideOpenCamera, ModuleCameraWide},
		{"wide photo", CmdWidePhotograph, ModuleCameraWide},
		{"wide boundary low", 12000, ModuleCameraWide},
		{"wide boundary high-1", 12499, ModuleCameraWide},

		// System (13000–13299)
		{"system set time", CmdSystemSetTime, ModuleSystem},
		{"system set location", CmdSystemSetLocation, ModuleSystem},
		{"system boundary low", 13000, ModuleSystem},
		{"system boundary high-1", 13299, ModuleSystem},

		// RGB Power (13500–13799)
		{"rgb on", CmdRGBOn, ModuleRGBPower},
		{"reboot", CmdReboot, ModuleRGBPower},
		{"rgb boundary low", 13500, ModuleRGBPower},
		{"rgb boundary high-1", 13799, ModuleRGBPower},

		// Motor (14000–14499)
		{"motor run", CmdMotorRun, ModuleMotor},
		{"motor joystick", CmdMotorJoystick, ModuleMotor},
		{"motor boundary low", 14000, ModuleMotor},
		{"motor boundary high-1", 14499, ModuleMotor},

		// Track (14800–14899)
		{"track start", CmdTrackStart, ModuleTrack},
		{"switch preview", CmdSwitchMainPreview, ModuleTrack},
		{"track boundary low", 14800, ModuleTrack},
		{"track boundary high-1", 14899, ModuleTrack},

		// Focus (15000–15199)
		{"focus auto", CmdFocusAutoFocus, ModuleFocus},
		{"focus boundary low", 15000, ModuleFocus},
		{"focus boundary high-1", 15199, ModuleFocus},

		// Notify (15200–15499)
		{"notify track result", NotifyTrackResult, ModuleNotify},
		{"notify boundary low", 15200, ModuleNotify},
		{"notify boundary high-1", 15499, ModuleNotify},

		// Panorama (15500–15599)
		{"pano start grid", CmdPanoStartGrid, ModulePanorama},
		{"pano boundary low", 15500, ModulePanorama},
		{"pano boundary high-1", 15599, ModulePanorama},

		// ITips (15700–15799)
		{"itips boundary low", 15700, ModuleITips},

		// Shooting Schedule (16100–16399)
		{"schedule boundary low", 16100, ModuleShootingSchedule},
		{"schedule boundary high-1", 16399, ModuleShootingSchedule},

		// Task Center (16400–16599)
		{"task get state", CmdTaskGetDeviceState, ModuleTaskCenter},
		{"task boundary low", 16400, ModuleTaskCenter},
		{"task boundary high-1", 16599, ModuleTaskCenter},

		// Param (16700–16799)
		{"param boundary low", 16700, ModuleParam},

		// Voice Assistant (16800–16899)
		{"voice boundary low", 16800, ModuleVoiceAssistant},

		// Device (17000–17099)
		{"device boundary low", 17000, ModuleDevice},
		{"device boundary high-1", 17099, ModuleDevice},

		// Out of range
		{"unknown cmd 9999", 9999, ModuleNone},
		{"unknown cmd 99999", 99999, ModuleNone},
		{"cmd 0", 0, ModuleNone},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := ModuleIDForCmd(tc.cmd)
			if got != tc.wantMod {
				t.Errorf("ModuleIDForCmd(%d) = %d, want %d", tc.cmd, got, tc.wantMod)
			}
		})
	}
}

// TestCameraCmdHelper verifies that cameraCmd dispatches correctly between
// Tele and Wide command IDs.
func TestCameraCmdHelper(t *testing.T) {
	cases := []struct {
		tele, wide uint32
		cam        Camera
		want       uint32
	}{
		{CmdTeleOpenCamera, CmdWideOpenCamera, CameraTele, CmdTeleOpenCamera},
		{CmdTeleOpenCamera, CmdWideOpenCamera, CameraWide, CmdWideOpenCamera},
		{10002, 12022, CameraTele, 10002},
		{10002, 12022, CameraWide, 12022},
	}
	for _, tc := range cases {
		got := cameraCmd(tc.tele, tc.wide, tc.cam)
		if got != tc.want {
			t.Errorf("cameraCmd(%d,%d,%d) = %d, want %d",
				tc.tele, tc.wide, tc.cam, got, tc.want)
		}
	}
}

// TestDecodeResponseEmpty verifies that decoding an empty Data field does
// not error and leaves the message at its zero value.
func TestDecodeResponseEmpty(t *testing.T) {
	pkt := &pb.WsPacket{Cmd: 10000, Data: nil}
	msg := &pb.ResGetAllParams{}
	if err := decodeResponse(pkt, msg); err != nil {
		t.Errorf("decodeResponse with empty data should not error: %v", err)
	}
}

// TestDecodeResponseWithPayload verifies that a response payload decodes
// correctly.
func TestDecodeResponseWithPayload(t *testing.T) {
	// Build a ComResponse{code: 0} as the inner payload
	inner := &pb.ComResponse{Code: 0}
	data, err := proto.Marshal(inner)
	if err != nil {
		t.Fatalf("marshal ComResponse: %v", err)
	}
	pkt := &pb.WsPacket{Cmd: 10002, Data: data}

	msg := &pb.ComResponse{}
	if err := decodeResponse(pkt, msg); err != nil {
		t.Fatalf("decodeResponse: %v", err)
	}
	if msg.GetCode() != 0 {
		t.Errorf("code: got %d, want 0", msg.GetCode())
	}
}

// TestDecodeResponseBadData verifies that corrupt data returns an error.
func TestDecodeResponseBadData(t *testing.T) {
	pkt := &pb.WsPacket{Data: []byte{0xff, 0xff, 0xff, 0xff}}
	msg := &pb.ComResponse{}
	// Invalid wire format should error (may or may not depending on content,
	// but definitely garbled). We just ensure it doesn't panic.
	_ = decodeResponse(pkt, msg)
}

// TestCommandIDUniqueness verifies that command IDs are unique across all
// modules (no accidental collisions).
func TestCommandIDUniqueness(t *testing.T) {
	allCmds := []uint32{
		CmdTeleOpenCamera, CmdTeleCloseCamera, CmdTelePhotograph, CmdTeleBurst,
		CmdTeleStopBurst, CmdTeleStartRecord, CmdTeleStopRecord,
		CmdTeleSetExp, CmdTeleGetExp, CmdTeleSetGain, CmdTeleGetGain,
		CmdAstroStartCalibration, CmdAstroStopCalibration,
		CmdAstroStartGotoDSO, CmdAstroStartGotoSolar, CmdAstroStopGoto,
		CmdWideOpenCamera, CmdWideCloseCamera, CmdWidePhotograph,
		CmdSystemSetTime, CmdSystemSetLocation,
		CmdRGBOn, CmdRGBOff, CmdPowerDown, CmdReboot,
		CmdMotorRun, CmdMotorStop, CmdMotorJoystick,
		CmdTrackStart, CmdTrackStop, CmdSwitchMainPreview,
		CmdFocusAutoFocus, CmdFocusManualSingle,
		CmdPanoStartGrid, CmdPanoStop,
		CmdTaskStartTask, CmdTaskStopTask, CmdTaskGetDeviceState,
	}
	seen := make(map[uint32]string)
	for _, cmd := range allCmds {
		if prev, ok := seen[cmd]; ok {
			t.Errorf("command ID %d is duplicated (%s vs %s)", cmd, prev, "current")
		}
		seen[cmd] = "seen"
	}
}

// TestNewTelescope verifies that New() returns a usable Telescope instance.
func TestNewTelescope(t *testing.T) {
	scope := New("test-client")
	if scope == nil {
		t.Fatal("New() returned nil")
	}
	if scope.t == nil {
		t.Fatal("telescope transport is nil")
	}
}

// TestModuleConstants verify module IDs are non-zero and distinct.
func TestModuleConstants(t *testing.T) {
	modules := []uint32{
		ModuleCameraTele, ModuleCameraWide, ModuleAstro, ModuleSystem,
		ModuleRGBPower, ModuleMotor, ModuleTrack, ModuleFocus, ModuleNotify,
		ModulePanorama, ModuleITips, ModuleShootingSchedule, ModuleTaskCenter,
		ModuleParam, ModuleVoiceAssistant, ModuleDevice,
	}
	seen := make(map[uint32]bool)
	for _, m := range modules {
		if m == 0 {
			t.Errorf("module constant is 0 (ModuleNone)")
		}
		if seen[m] {
			t.Errorf("module constant %d is duplicated", m)
		}
		seen[m] = true
	}
}
