package ws_test

import (
	"context"
	"encoding/json"
	"testing"

	"github.com/owncord/server/db"
	"github.com/owncord/server/ws"
)

// ─── IsUserConnected ────────────────────────────────────────────────────────

func TestIsUserConnected_NotConnected(t *testing.T) {
	hub, _ := newCoverageHub(t)

	if hub.IsUserConnected(9999) {
		t.Error("expected false for unregistered user")
	}
}

func TestIsUserConnected_Connected(t *testing.T) {
	hub, database := newCoverageHub(t)
	user := seedCoverageOwner(t, database, "connected-user")
	send := make(chan []byte, 16)
	c := ws.NewTestClientWithUser(hub, user, 0, send)
	hub.Register(c)
	waitRegistered(t, hub, c)

	if !hub.IsUserConnected(user.ID) {
		t.Error("expected true for registered user")
	}
}

func TestIsUserConnected_AfterUnregister(t *testing.T) {
	hub, database := newCoverageHub(t)
	user := seedCoverageOwner(t, database, "unreg-user")
	send := make(chan []byte, 16)
	c := ws.NewTestClientWithUser(hub, user, 0, send)
	hub.Register(c)
	waitRegistered(t, hub, c)

	hub.Unregister(c)
	waitFor(t, waitTimeout, func() bool { return !hub.IsUserConnected(user.ID) },
		"client to be unregistered")

	if hub.IsUserConnected(user.ID) {
		t.Error("expected false after unregister")
	}
}

// ─── qualityBitrate ─────────────────────────────────────────────────────────

func TestQualityBitrate_KnownPresets(t *testing.T) {
	tests := []struct {
		quality string
		want    int
	}{
		{"low", 32000},
		{"medium", 64000},
		{"high", 128000},
	}
	for _, tc := range tests {
		got := ws.QualityBitrateForTest(tc.quality)
		if got != tc.want {
			t.Errorf("qualityBitrate(%q) = %d, want %d", tc.quality, got, tc.want)
		}
	}
}

func TestQualityBitrate_UnknownFallsBackToMedium(t *testing.T) {
	got := ws.QualityBitrateForTest("ultra")
	if got != 64000 {
		t.Errorf("qualityBitrate('ultra') = %d, want 64000 (medium fallback)", got)
	}
}

func TestQualityBitrate_EmptyFallsBackToMedium(t *testing.T) {
	got := ws.QualityBitrateForTest("")
	if got != 64000 {
		t.Errorf("qualityBitrate('') = %d, want 64000 (medium fallback)", got)
	}
}

// ─── buildDMChannelOpen ─────────────────────────────────────────────────────

func TestBuildDMChannelOpen_NilRecipient(t *testing.T) {
	result := ws.BuildDMChannelOpenForTest(1, nil)
	if result != nil {
		t.Error("expected nil for nil recipient")
	}
}

func TestBuildDMChannelOpen_ValidRecipient(t *testing.T) {
	avatar := "avatar.png"
	user := &db.User{
		ID:       42,
		Username: "testuser",
		Avatar:   &avatar,
		Status:   "online",
	}

	result := ws.BuildDMChannelOpenForTest(100, user)
	if result == nil {
		t.Fatal("expected non-nil result for valid recipient")
	}
	if !json.Valid(result) {
		t.Fatalf("result is not valid JSON: %s", result)
	}

	var msg struct {
		Type    string `json:"type"`
		Payload struct {
			ChannelID int64 `json:"channel_id"`
			Recipient struct {
				ID       int64  `json:"id"`
				Username string `json:"username"`
				Avatar   string `json:"avatar"`
				Status   string `json:"status"`
			} `json:"recipient"`
		} `json:"payload"`
	}
	if err := json.Unmarshal(result, &msg); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if msg.Payload.ChannelID != 100 {
		t.Errorf("ChannelID = %d, want 100", msg.Payload.ChannelID)
	}
	if msg.Payload.Recipient.ID != 42 {
		t.Errorf("Recipient.ID = %d, want 42", msg.Payload.Recipient.ID)
	}
	if msg.Payload.Recipient.Username != "testuser" {
		t.Errorf("Username = %q, want 'testuser'", msg.Payload.Recipient.Username)
	}
	if msg.Payload.Recipient.Avatar != "avatar.png" {
		t.Errorf("Avatar = %q, want 'avatar.png'", msg.Payload.Recipient.Avatar)
	}
}

func TestBuildDMChannelOpen_NilAvatar(t *testing.T) {
	user := &db.User{
		ID:       43,
		Username: "noavatar",
		Avatar:   nil,
		Status:   "offline",
	}

	result := ws.BuildDMChannelOpenForTest(200, user)
	if result == nil {
		t.Fatal("expected non-nil result")
	}

	var msg struct {
		Payload struct {
			Recipient struct {
				Avatar string `json:"avatar"`
			} `json:"recipient"`
		} `json:"payload"`
	}
	_ = json.Unmarshal(result, &msg)
	if msg.Payload.Recipient.Avatar != "" {
		t.Errorf("Avatar = %q, want empty string for nil avatar", msg.Payload.Recipient.Avatar)
	}
}

// ─── handleVoiceMute via HandleMessageForTest ───────────────────────────────

func TestHandleVoiceMute_NotInVoice2(t *testing.T) {
	hub, database := newCoverageHub(t)
	user := seedCoverageOwner(t, database, "mute-not-in-voice")
	send := make(chan []byte, 16)
	c := ws.NewTestClientWithUser(hub, user, 0, send)
	hub.RegisterNowForTest(c)

	payload := `{"muted":true}`
	raw, _ := json.Marshal(map[string]any{"type": "voice_mute", "payload": json.RawMessage(payload)})
	hub.HandleMessageForTest(c, raw)

	// Should receive an error about not being in a voice channel.
	// Error replies are sent synchronously by handleMessage — already buffered.
	found := false
	for len(send) > 0 {
		msg := <-send
		var m struct {
			Type    string `json:"type"`
			Payload struct {
				Message string `json:"message"`
			} `json:"payload"`
		}
		_ = json.Unmarshal(msg, &m)
		if m.Type == "error" {
			found = true
		}
	}
	if !found {
		t.Error("expected error message when not in voice channel")
	}
}

// ─── handleVoiceDeafen not in voice ─────────────────────────────────────────

func TestHandleVoiceDeafen_NotInVoice2(t *testing.T) {
	hub, database := newCoverageHub(t)
	user := seedCoverageOwner(t, database, "deafen-not-voice")
	send := make(chan []byte, 16)
	c := ws.NewTestClientWithUser(hub, user, 0, send)
	hub.RegisterNowForTest(c)

	payload := `{"deafened":true}`
	raw, _ := json.Marshal(map[string]any{"type": "voice_deafen", "payload": json.RawMessage(payload)})
	hub.HandleMessageForTest(c, raw)

	// Error replies are sent synchronously by handleMessage — already buffered.
	found := false
	for len(send) > 0 {
		msg := <-send
		var m struct {
			Type string `json:"type"`
		}
		_ = json.Unmarshal(msg, &m)
		if m.Type == "error" {
			found = true
		}
	}
	if !found {
		t.Error("expected error message when not in voice channel")
	}
}

// ─── handleVoiceCamera not in voice ─────────────────────────────────────────

func TestHandleVoiceCamera_NotInVoice2(t *testing.T) {
	hub, database := newCoverageHub(t)
	user := seedCoverageOwner(t, database, "cam-not-voice")
	send := make(chan []byte, 16)
	c := ws.NewTestClientWithUser(hub, user, 0, send)
	hub.RegisterNowForTest(c)

	payload := `{"enabled":true}`
	raw, _ := json.Marshal(map[string]any{"type": "voice_camera", "payload": json.RawMessage(payload)})
	hub.HandleMessageForTest(c, raw)

	// Error replies are sent synchronously by handleMessage — already buffered.
	found := false
	for len(send) > 0 {
		msg := <-send
		var m struct {
			Type string `json:"type"`
		}
		_ = json.Unmarshal(msg, &m)
		if m.Type == "error" {
			found = true
		}
	}
	if !found {
		t.Error("expected error message when not in voice channel")
	}
}

// ─── handleVoiceScreenshare not in voice ────────────────────────────────────

func TestHandleVoiceScreenshare_NotInVoice2(t *testing.T) {
	hub, database := newCoverageHub(t)
	user := seedCoverageOwner(t, database, "share-not-voice")
	send := make(chan []byte, 16)
	c := ws.NewTestClientWithUser(hub, user, 0, send)
	hub.RegisterNowForTest(c)

	payload := `{"enabled":true}`
	raw, _ := json.Marshal(map[string]any{"type": "voice_screenshare", "payload": json.RawMessage(payload)})
	hub.HandleMessageForTest(c, raw)

	// Error replies are sent synchronously by handleMessage — already buffered.
	found := false
	for len(send) > 0 {
		msg := <-send
		var m struct {
			Type string `json:"type"`
		}
		_ = json.Unmarshal(msg, &m)
		if m.Type == "error" {
			found = true
		}
	}
	if !found {
		t.Error("expected error message when not in voice channel")
	}
}

// ─── handleVoiceMute/Deafen bad payload ─────────────────────────────────────

func TestHandleVoiceMute_BadPayload(t *testing.T) {
	hub, database := newCoverageHub(t)
	user := seedCoverageOwner(t, database, "mute-bad-payload")
	chanID, _ := database.CreateChannel(context.Background(), "mute-bp-ch", "voice", "", "", 0)
	_ = database.JoinVoiceChannel(context.Background(), user.ID, chanID)

	send := make(chan []byte, 16)
	c := ws.NewTestClientWithUser(hub, user, 0, send)
	ws.SetClientVoiceChID(c, chanID)
	hub.RegisterNowForTest(c)

	raw, _ := json.Marshal(map[string]any{"type": "voice_mute", "payload": json.RawMessage(`{invalid json`)})
	hub.HandleMessageForTest(c, raw)

	// Error replies are sent synchronously by handleMessage — already buffered.
	found := false
	for len(send) > 0 {
		msg := <-send
		var m struct {
			Type string `json:"type"`
		}
		_ = json.Unmarshal(msg, &m)
		if m.Type == "error" {
			found = true
		}
	}
	if !found {
		t.Error("expected error for bad payload")
	}
}

func TestHandleVoiceDeafen_BadPayload(t *testing.T) {
	hub, database := newCoverageHub(t)
	user := seedCoverageOwner(t, database, "deafen-bad-payload")
	chanID, _ := database.CreateChannel(context.Background(), "deafen-bp-ch", "voice", "", "", 0)
	_ = database.JoinVoiceChannel(context.Background(), user.ID, chanID)

	send := make(chan []byte, 16)
	c := ws.NewTestClientWithUser(hub, user, 0, send)
	ws.SetClientVoiceChID(c, chanID)
	hub.RegisterNowForTest(c)

	raw, _ := json.Marshal(map[string]any{"type": "voice_deafen", "payload": json.RawMessage(`not json`)})
	hub.HandleMessageForTest(c, raw)

	// Error replies are sent synchronously by handleMessage — already buffered.
	found := false
	for len(send) > 0 {
		msg := <-send
		var m struct {
			Type string `json:"type"`
		}
		_ = json.Unmarshal(msg, &m)
		if m.Type == "error" {
			found = true
		}
	}
	if !found {
		t.Error("expected error for bad payload")
	}
}

// ─── handleVoiceCamera bad payload ──────────────────────────────────────────

func TestHandleVoiceCamera_BadPayload(t *testing.T) {
	hub, database := newCoverageHub(t)
	user := seedCoverageOwner(t, database, "cam-bad-payload")
	chanID, _ := database.CreateChannel(context.Background(), "cam-bp-ch", "voice", "", "", 0)
	_ = database.JoinVoiceChannel(context.Background(), user.ID, chanID)

	send := make(chan []byte, 16)
	c := ws.NewTestClientWithUser(hub, user, 0, send)
	ws.SetClientVoiceChID(c, chanID)
	ws.SetClientVoiceStateForTest(c, chanID, "join-token-fake")
	hub.RegisterNowForTest(c)

	raw, _ := json.Marshal(map[string]any{"type": "voice_camera", "payload": json.RawMessage(`{bad`)})
	hub.HandleMessageForTest(c, raw)

	// Error replies are sent synchronously by handleMessage — already buffered.
	found := false
	for len(send) > 0 {
		msg := <-send
		var m struct {
			Type string `json:"type"`
		}
		_ = json.Unmarshal(msg, &m)
		if m.Type == "error" {
			found = true
		}
	}
	if !found {
		t.Error("expected error for bad payload")
	}
}

// ─── handleVoiceScreenshare bad payload ─────────────────────────────────────

func TestHandleVoiceScreenshare_BadPayload(t *testing.T) {
	hub, database := newCoverageHub(t)
	user := seedCoverageOwner(t, database, "share-bad-payload")
	chanID, _ := database.CreateChannel(context.Background(), "share-bp-ch", "voice", "", "", 0)
	_ = database.JoinVoiceChannel(context.Background(), user.ID, chanID)

	send := make(chan []byte, 16)
	c := ws.NewTestClientWithUser(hub, user, 0, send)
	ws.SetClientVoiceChID(c, chanID)
	ws.SetClientVoiceStateForTest(c, chanID, "join-token-fake")
	hub.RegisterNowForTest(c)

	raw, _ := json.Marshal(map[string]any{"type": "voice_screenshare", "payload": json.RawMessage(`{bad`)})
	hub.HandleMessageForTest(c, raw)

	// Error replies are sent synchronously by handleMessage — already buffered.
	found := false
	for len(send) > 0 {
		msg := <-send
		var m struct {
			Type string `json:"type"`
		}
		_ = json.Unmarshal(msg, &m)
		if m.Type == "error" {
			found = true
		}
	}
	if !found {
		t.Error("expected error for bad payload")
	}
}

// ─── leaveVoiceChannelWithRetry empty token ─────────────────────────────────

func TestLeaveVoiceChannelWithRetry_EmptyToken(t *testing.T) {
	hub, _ := newCoverageHub(t)

	err := ws.LeaveVoiceChannelWithRetryForTest(hub, 1, 1, "")
	if err != nil {
		t.Errorf("expected nil error for empty token, got %v", err)
	}
}
