package ws

import (
	"encoding/json"
	"testing"
)

// allClientToServerTypes returns every client-to-server message type constant
// that should have a CommandConstructor entry (excludes "auth" which is handled
// separately by the auth flow).
func allClientToServerTypes() []string {
	return []string{
		MsgTypePing,
		MsgTypeChatSend,
		MsgTypeChatEdit,
		MsgTypeChatDelete,
		MsgTypeTypingStart,
		MsgTypePresenceUpdate,
		MsgTypeChannelFocus,
		MsgTypeReactionAdd,
		MsgTypeReactionRemove,
		MsgTypeVoiceJoin,
		MsgTypeVoiceLeave,
		MsgTypeVoiceTokenRefresh,
		MsgTypeVoiceMute,
		MsgTypeVoiceDeafen,
		MsgTypeVoiceCamera,
		MsgTypeVoiceScreenshare,
		MsgTypeVoiceE2EEAnnounce,
		MsgTypeVoiceE2EEOffer,
	}
}

func TestCommandConstructorsCoverage(t *testing.T) {
	for _, msgType := range allClientToServerTypes() {
		if _, ok := commandConstructors[msgType]; !ok {
			t.Errorf("commandConstructors missing entry for %q", msgType)
		}
	}
}

func TestCommandTypeAndUserID(t *testing.T) {
	tests := []struct {
		name     string
		cmd      Command
		wantType string
		wantUID  int64
	}{
		{"PingCmd", PingCmd{userID: 1}, MsgTypePing, 1},
		{"ChatSendCmd", ChatSendCmd{userID: 2, channelID: 10}, MsgTypeChatSend, 2},
		{"ChatEditCmd", ChatEditCmd{userID: 3, messageID: 20}, MsgTypeChatEdit, 3},
		{"ChatDeleteCmd", ChatDeleteCmd{userID: 4, messageID: 30}, MsgTypeChatDelete, 4},
		{"TypingStartCmd", TypingStartCmd{userID: 5, channelID: 11}, MsgTypeTypingStart, 5},
		{"PresenceUpdateCmd", PresenceUpdateCmd{userID: 6, status: "online"}, MsgTypePresenceUpdate, 6},
		{"ChannelFocusCmd", ChannelFocusCmd{userID: 7, channelID: 12}, MsgTypeChannelFocus, 7},
		{"ReactionAddCmd", ReactionAddCmd{userID: 8, messageID: 40, emoji: "👍"}, MsgTypeReactionAdd, 8},
		{"ReactionRemoveCmd", ReactionRemoveCmd{userID: 9, messageID: 41, emoji: "👎"}, MsgTypeReactionRemove, 9},
		{"VoiceJoinCmd", VoiceJoinCmd{userID: 10, channelID: 13}, MsgTypeVoiceJoin, 10},
		{"VoiceLeaveCmd", VoiceLeaveCmd{userID: 11}, MsgTypeVoiceLeave, 11},
		{"VoiceTokenRefreshCmd", VoiceTokenRefreshCmd{userID: 12}, MsgTypeVoiceTokenRefresh, 12},
		{"VoiceMuteCmd", VoiceMuteCmd{userID: 13, muted: true}, MsgTypeVoiceMute, 13},
		{"VoiceDeafenCmd", VoiceDeafenCmd{userID: 14, deafened: true}, MsgTypeVoiceDeafen, 14},
		{"VoiceCameraCmd", VoiceCameraCmd{userID: 15, enabled: true}, MsgTypeVoiceCamera, 15},
		{"VoiceScreenshareCmd", VoiceScreenshareCmd{userID: 16, enabled: true}, MsgTypeVoiceScreenshare, 16},
		{"VoiceE2EEAnnounceCmd", VoiceE2EEAnnounceCmd{userID: 17, publicKey: "abc"}, MsgTypeVoiceE2EEAnnounce, 17},
		{"VoiceE2EEOfferCmd", VoiceE2EEOfferCmd{userID: 18, targetUserID: 99}, MsgTypeVoiceE2EEOffer, 18},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := tt.cmd.Type(); got != tt.wantType {
				t.Errorf("Type() = %q, want %q", got, tt.wantType)
			}
			if got := tt.cmd.UserID(); got != tt.wantUID {
				t.Errorf("UserID() = %d, want %d", got, tt.wantUID)
			}
		})
	}
}

func TestCommandChannelScoped(t *testing.T) {
	tests := []struct {
		name     string
		cmd      Command
		wantChID int64
		isScoped bool
	}{
		{"ChatSendCmd", ChatSendCmd{channelID: 100}, 100, true},
		{"TypingStartCmd", TypingStartCmd{channelID: 200}, 200, true},
		{"ChannelFocusCmd", ChannelFocusCmd{channelID: 300}, 300, true},
		{"VoiceJoinCmd", VoiceJoinCmd{channelID: 400}, 400, true},
		{"PingCmd", PingCmd{userID: 1}, 0, false},
		{"VoiceLeaveCmd", VoiceLeaveCmd{userID: 1}, 0, false},
		{"PresenceUpdateCmd", PresenceUpdateCmd{userID: 1}, 0, false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cs, ok := tt.cmd.(ChannelScoped)
			if ok != tt.isScoped {
				t.Errorf("ChannelScoped assertion = %v, want %v", ok, tt.isScoped)
			}
			if ok && cs.ChannelID() != tt.wantChID {
				t.Errorf("ChannelID() = %d, want %d", cs.ChannelID(), tt.wantChID)
			}
		})
	}
}

func TestCommandConstructorParseValid(t *testing.T) {
	tests := []struct {
		name    string
		msgType string
		payload string
		checkFn func(t *testing.T, cmd Command)
	}{
		{
			name:    "ping",
			msgType: MsgTypePing,
			payload: `{}`,
			checkFn: func(t *testing.T, cmd Command) {
				if cmd.Type() != MsgTypePing {
					t.Errorf("Type() = %q, want %q", cmd.Type(), MsgTypePing)
				}
			},
		},
		{
			name:    "chat_send",
			msgType: MsgTypeChatSend,
			payload: `{"channel_id": 42, "content": "hello", "reply_to": 10, "attachments": ["a1"]}`,
			checkFn: func(t *testing.T, cmd Command) {
				cs := cmd.(ChatSendCmd)
				if cs.ChannelID() != 42 {
					t.Errorf("ChannelID() = %d, want 42", cs.ChannelID())
				}
				if cs.Content() != "hello" {
					t.Errorf("Content() = %q, want %q", cs.Content(), "hello")
				}
				if cs.ReplyTo() == nil || *cs.ReplyTo() != 10 {
					t.Errorf("ReplyTo() = %v, want 10", cs.ReplyTo())
				}
				if len(cs.Attachments()) != 1 || cs.Attachments()[0] != "a1" {
					t.Errorf("Attachments() = %v, want [a1]", cs.Attachments())
				}
			},
		},
		{
			name:    "chat_edit",
			msgType: MsgTypeChatEdit,
			payload: `{"message_id": 99, "content": "updated"}`,
			checkFn: func(t *testing.T, cmd Command) {
				ce := cmd.(ChatEditCmd)
				if ce.MessageID() != 99 {
					t.Errorf("MessageID() = %d, want 99", ce.MessageID())
				}
				if ce.Content() != "updated" {
					t.Errorf("Content() = %q, want %q", ce.Content(), "updated")
				}
			},
		},
		{
			name:    "chat_delete",
			msgType: MsgTypeChatDelete,
			payload: `{"message_id": 55}`,
			checkFn: func(t *testing.T, cmd Command) {
				cd := cmd.(ChatDeleteCmd)
				if cd.MessageID() != 55 {
					t.Errorf("MessageID() = %d, want 55", cd.MessageID())
				}
			},
		},
		{
			name:    "typing_start",
			msgType: MsgTypeTypingStart,
			payload: `{"channel_id": 7}`,
			checkFn: func(t *testing.T, cmd Command) {
				ts := cmd.(TypingStartCmd)
				if ts.ChannelID() != 7 {
					t.Errorf("ChannelID() = %d, want 7", ts.ChannelID())
				}
			},
		},
		{
			name:    "presence_update",
			msgType: MsgTypePresenceUpdate,
			payload: `{"status": "idle"}`,
			checkFn: func(t *testing.T, cmd Command) {
				pu := cmd.(PresenceUpdateCmd)
				if pu.Status() != "idle" {
					t.Errorf("Status() = %q, want %q", pu.Status(), "idle")
				}
			},
		},
		{
			name:    "channel_focus",
			msgType: MsgTypeChannelFocus,
			payload: `{"channel_id": 33}`,
			checkFn: func(t *testing.T, cmd Command) {
				cf := cmd.(ChannelFocusCmd)
				if cf.ChannelID() != 33 {
					t.Errorf("ChannelID() = %d, want 33", cf.ChannelID())
				}
			},
		},
		{
			name:    "reaction_add",
			msgType: MsgTypeReactionAdd,
			payload: `{"message_id": 77, "emoji": "🔥"}`,
			checkFn: func(t *testing.T, cmd Command) {
				ra := cmd.(ReactionAddCmd)
				if ra.MessageID() != 77 {
					t.Errorf("MessageID() = %d, want 77", ra.MessageID())
				}
				if ra.Emoji() != "🔥" {
					t.Errorf("Emoji() = %q, want %q", ra.Emoji(), "🔥")
				}
			},
		},
		{
			name:    "reaction_remove",
			msgType: MsgTypeReactionRemove,
			payload: `{"message_id": 88, "emoji": "👎"}`,
			checkFn: func(t *testing.T, cmd Command) {
				rr := cmd.(ReactionRemoveCmd)
				if rr.MessageID() != 88 {
					t.Errorf("MessageID() = %d, want 88", rr.MessageID())
				}
			},
		},
		{
			name:    "voice_join",
			msgType: MsgTypeVoiceJoin,
			payload: `{"channel_id": 50}`,
			checkFn: func(t *testing.T, cmd Command) {
				vj := cmd.(VoiceJoinCmd)
				if vj.ChannelID() != 50 {
					t.Errorf("ChannelID() = %d, want 50", vj.ChannelID())
				}
			},
		},
		{
			name:    "voice_leave",
			msgType: MsgTypeVoiceLeave,
			payload: `{}`,
			checkFn: func(t *testing.T, cmd Command) {
				if cmd.Type() != MsgTypeVoiceLeave {
					t.Errorf("Type() = %q, want %q", cmd.Type(), MsgTypeVoiceLeave)
				}
			},
		},
		{
			name:    "voice_token_refresh",
			msgType: MsgTypeVoiceTokenRefresh,
			payload: `{}`,
			checkFn: func(t *testing.T, cmd Command) {
				if cmd.Type() != MsgTypeVoiceTokenRefresh {
					t.Errorf("Type() = %q, want %q", cmd.Type(), MsgTypeVoiceTokenRefresh)
				}
			},
		},
		{
			name:    "voice_mute",
			msgType: MsgTypeVoiceMute,
			payload: `{"muted": true}`,
			checkFn: func(t *testing.T, cmd Command) {
				vm := cmd.(VoiceMuteCmd)
				if !vm.Muted() {
					t.Error("Muted() = false, want true")
				}
			},
		},
		{
			name:    "voice_deafen",
			msgType: MsgTypeVoiceDeafen,
			payload: `{"deafened": true}`,
			checkFn: func(t *testing.T, cmd Command) {
				vd := cmd.(VoiceDeafenCmd)
				if !vd.Deafened() {
					t.Error("Deafened() = false, want true")
				}
			},
		},
		{
			name:    "voice_camera",
			msgType: MsgTypeVoiceCamera,
			payload: `{"enabled": true}`,
			checkFn: func(t *testing.T, cmd Command) {
				vc := cmd.(VoiceCameraCmd)
				if !vc.Enabled() {
					t.Error("Enabled() = false, want true")
				}
			},
		},
		{
			name:    "voice_screenshare",
			msgType: MsgTypeVoiceScreenshare,
			payload: `{"enabled": false}`,
			checkFn: func(t *testing.T, cmd Command) {
				vs := cmd.(VoiceScreenshareCmd)
				if vs.Enabled() {
					t.Error("Enabled() = true, want false")
				}
			},
		},
		{
			name:    "voice_e2ee_announce",
			msgType: MsgTypeVoiceE2EEAnnounce,
			payload: `{"public_key": "dGVzdA=="}`,
			checkFn: func(t *testing.T, cmd Command) {
				va := cmd.(VoiceE2EEAnnounceCmd)
				if va.PublicKey() != "dGVzdA==" {
					t.Errorf("PublicKey() = %q, want %q", va.PublicKey(), "dGVzdA==")
				}
			},
		},
		{
			name:    "voice_e2ee_offer",
			msgType: MsgTypeVoiceE2EEOffer,
			payload: `{"target_user_id": 99, "encrypted_key": "abc", "iv": "def"}`,
			checkFn: func(t *testing.T, cmd Command) {
				vo := cmd.(VoiceE2EEOfferCmd)
				if vo.TargetUserID() != 99 {
					t.Errorf("TargetUserID() = %d, want 99", vo.TargetUserID())
				}
				if vo.EncryptedKey() != "abc" {
					t.Errorf("EncryptedKey() = %q, want %q", vo.EncryptedKey(), "abc")
				}
				if vo.IV() != "def" {
					t.Errorf("IV() = %q, want %q", vo.IV(), "def")
				}
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctor, ok := commandConstructors[tt.msgType]
			if !ok {
				t.Fatalf("no constructor for %q", tt.msgType)
			}
			cmd, err := ctor(42, "req-1", json.RawMessage(tt.payload))
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
			if cmd.UserID() != 42 {
				t.Errorf("UserID() = %d, want 42", cmd.UserID())
			}
			tt.checkFn(t, cmd)
		})
	}
}

func TestCommandConstructorRejectsInvalidJSON(t *testing.T) {
	// All constructors that parse a payload should reject garbage JSON.
	typesWithPayload := []string{
		MsgTypeChatSend,
		MsgTypeChatEdit,
		MsgTypeChatDelete,
		MsgTypeTypingStart,
		MsgTypePresenceUpdate,
		MsgTypeChannelFocus,
		MsgTypeReactionAdd,
		MsgTypeReactionRemove,
		MsgTypeVoiceJoin,
		MsgTypeVoiceMute,
		MsgTypeVoiceDeafen,
		MsgTypeVoiceCamera,
		MsgTypeVoiceScreenshare,
		MsgTypeVoiceE2EEAnnounce,
		MsgTypeVoiceE2EEOffer,
	}
	badJSON := json.RawMessage(`{not valid json`)
	for _, msgType := range typesWithPayload {
		t.Run(msgType, func(t *testing.T) {
			ctor := commandConstructors[msgType]
			_, err := ctor(1, "req-1", badJSON)
			if err == nil {
				t.Errorf("expected error for invalid JSON, got nil")
			}
		})
	}
}

func TestCommandConstructorNoPayloadTypes(t *testing.T) {
	// These types ignore the payload — they should succeed with nil.
	noPayloadTypes := []string{
		MsgTypePing,
		MsgTypeVoiceLeave,
		MsgTypeVoiceTokenRefresh,
	}
	for _, msgType := range noPayloadTypes {
		t.Run(msgType, func(t *testing.T) {
			ctor := commandConstructors[msgType]
			cmd, err := ctor(5, "", nil)
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
			if cmd.UserID() != 5 {
				t.Errorf("UserID() = %d, want 5", cmd.UserID())
			}
		})
	}
}

func TestCommandChatSendReqID(t *testing.T) {
	ctor := commandConstructors[MsgTypeChatSend]
	cmd, err := ctor(1, "abc-123", json.RawMessage(`{"channel_id": 1, "content": "hi"}`))
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	cs := cmd.(ChatSendCmd)
	if cs.ReqID() != "abc-123" {
		t.Errorf("ReqID() = %q, want %q", cs.ReqID(), "abc-123")
	}
}

func TestCommandChatSendNilReplyTo(t *testing.T) {
	ctor := commandConstructors[MsgTypeChatSend]
	cmd, err := ctor(1, "", json.RawMessage(`{"channel_id": 1, "content": "hi"}`))
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	cs := cmd.(ChatSendCmd)
	if cs.ReplyTo() != nil {
		t.Errorf("ReplyTo() = %v, want nil", cs.ReplyTo())
	}
}

func TestCommandAttachmentsDefensiveCopy(t *testing.T) {
	cmd := ChatSendCmd{attachments: []string{"a", "b"}}
	got := cmd.Attachments()
	got[0] = "mutated"
	if cmd.attachments[0] == "mutated" {
		t.Error("Attachments() did not return a defensive copy")
	}
}
