package ws_test

// coverage_chat_test.go: chat send/edit/delete, reactions, slow mode, and
// read-state coverage tests (split from coverage_boost_test.go).

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

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

// ─── handleChatSend additional branches (handlers.go:127 — 76.2%) ────────────

func TestHandleChatSend_EmptyContent(t *testing.T) {
	hub, database := newCoverageHub(t)
	user := seedCoverageOwner(t, database, "empty-content-user")
	chID := seedTestChannel(t, database, "empty-content-chan")
	send := make(chan []byte, 16)
	c := ws.NewTestClientWithUser(hub, user, chID, send)
	hub.Register(c)
	waitRegistered(t, hub, c)

	raw, _ := json.Marshal(map[string]any{
		"type": "chat_send",
		"payload": map[string]any{
			"channel_id": chID,
			"content":    "",
		},
	})
	hub.HandleMessageForTest(c, raw)

	code := drainForErrorCode(send, 200*time.Millisecond)
	if code != "BAD_REQUEST" {
		t.Errorf("error code = %q, want BAD_REQUEST for empty content", code)
	}
}

func TestHandleChatSend_ContentTooLong(t *testing.T) {
	hub, database := newCoverageHub(t)
	user := seedCoverageOwner(t, database, "long-content-user")
	chID := seedTestChannel(t, database, "long-content-chan")
	send := make(chan []byte, 16)
	c := ws.NewTestClientWithUser(hub, user, chID, send)
	hub.Register(c)
	waitRegistered(t, hub, c)

	// Content over 4000 characters.
	longContent := strings.Repeat("x", 4001)
	raw, _ := json.Marshal(map[string]any{
		"type": "chat_send",
		"payload": map[string]any{
			"channel_id": chID,
			"content":    longContent,
		},
	})
	hub.HandleMessageForTest(c, raw)

	code := drainForErrorCode(send, 200*time.Millisecond)
	if code != "BAD_REQUEST" {
		t.Errorf("error code = %q, want BAD_REQUEST for content too long", code)
	}
}

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

	raw, _ := json.Marshal(map[string]any{
		"type": "chat_send",
		"payload": map[string]any{
			"channel_id": "not-a-number",
			"content":    "hello",
		},
	})
	hub.HandleMessageForTest(c, raw)

	code := drainForErrorCode(send, 200*time.Millisecond)
	if code != "BAD_REQUEST" {
		t.Errorf("error code = %q, want BAD_REQUEST for invalid channel_id", code)
	}
}

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

	raw, _ := json.Marshal(map[string]any{
		"type": "chat_send",
		"payload": map[string]any{
			"channel_id": 99999,
			"content":    "hello",
		},
	})
	hub.HandleMessageForTest(c, raw)

	code := drainForErrorCode(send, 200*time.Millisecond)
	if code != "NOT_FOUND" {
		t.Errorf("error code = %q, want NOT_FOUND for nonexistent channel", code)
	}
}

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

	raw, _ := json.Marshal(map[string]any{
		"type":    "chat_send",
		"payload": "not-an-object",
	})
	hub.HandleMessageForTest(c, raw)

	code := drainForErrorCode(send, 200*time.Millisecond)
	if code != "BAD_REQUEST" {
		t.Errorf("error code = %q, want BAD_REQUEST for invalid payload", code)
	}
}

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

	raw, _ := json.Marshal(map[string]any{
		"type": "chat_send",
		"payload": map[string]any{
			"channel_id": -1,
			"content":    "hello",
		},
	})
	hub.HandleMessageForTest(c, raw)

	code := drainForErrorCode(send, 200*time.Millisecond)
	if code != "BAD_REQUEST" {
		t.Errorf("error code = %q, want BAD_REQUEST for negative channel_id", code)
	}
}

// ─── handleChatSend with reply_to (handlers.go:127 — covers reply_to path) ──

func TestHandleChatSend_WithReplyTo(t *testing.T) {
	hub, database := newCoverageHub(t)
	user := seedCoverageOwner(t, database, "reply-user")
	chID := seedTestChannel(t, database, "reply-chan")
	send := make(chan []byte, 32)
	c := ws.NewTestClientWithUser(hub, user, chID, send)
	hub.Register(c)
	waitRegistered(t, hub, c)

	// Send first message to get an ID.
	raw1, _ := json.Marshal(map[string]any{
		"type": "chat_send",
		"id":   "req-1",
		"payload": map[string]any{
			"channel_id": chID,
			"content":    "original message",
		},
	})
	hub.HandleMessageForTest(c, raw1)

	// Drain to find the message ID from chat_send_ok.
	var msgID float64
	timeout := time.After(500 * time.Millisecond)
drainFirst:
	for {
		select {
		case msg := <-send:
			var env map[string]any
			if err := json.Unmarshal(msg, &env); err == nil {
				if env["type"] == "chat_send_ok" {
					if p, ok := env["payload"].(map[string]any); ok {
						msgID = p["message_id"].(float64)
					}
					break drainFirst
				}
			}
		case <-timeout:
			t.Fatal("did not receive chat_send_ok for first message")
		}
	}

	// Drain remaining messages.
	drainChanBuf(send)

	// Send reply.
	replyTo := int64(msgID)
	raw2, _ := json.Marshal(map[string]any{
		"type": "chat_send",
		"id":   "req-2",
		"payload": map[string]any{
			"channel_id": chID,
			"content":    "reply message",
			"reply_to":   replyTo,
		},
	})
	hub.HandleMessageForTest(c, raw2)

	// Should get chat_send_ok for the reply.
	found := false
	timeout2 := time.After(500 * time.Millisecond)
drainReply:
	for {
		select {
		case msg := <-send:
			var env map[string]any
			if err := json.Unmarshal(msg, &env); err == nil {
				if env["type"] == "chat_send_ok" && env["id"] == "req-2" {
					found = true
					break drainReply
				}
			}
		case <-timeout2:
			break drainReply
		}
	}
	if !found {
		t.Error("expected chat_send_ok for reply message")
	}
}

// ─── handleChatSend slow mode for non-mod user (handlers.go:164) ────────────

func TestHandleChatSend_SlowMode_EnforcedForMember(t *testing.T) {
	hub, database := newCoverageHub(t)
	_, err := database.CreateUser(context.Background(), "slow-member-user", "hash", 4)
	if err != nil {
		t.Fatalf("CreateUser: %v", err)
	}
	user, err := database.GetUserByUsername(context.Background(), "slow-member-user")
	if err != nil || user == nil {
		t.Fatalf("GetUserByUsername: %v", err)
	}

	// Create channel with slow mode.
	chID, err := database.CreateChannel(context.Background(), "slow-chan", "text", "", "", 0)
	if err != nil {
		t.Fatalf("CreateChannel: %v", err)
	}
	if err := database.SetChannelSlowMode(context.Background(), chID, 60); err != nil {
		t.Fatalf("SetChannelSlowMode: %v", err)
	}

	send := make(chan []byte, 64)
	c := ws.NewTestClientWithUser(hub, user, chID, send)
	hub.Register(c)
	waitRegistered(t, hub, c)

	raw, _ := json.Marshal(map[string]any{
		"type": "chat_send",
		"payload": map[string]any{
			"channel_id": chID,
			"content":    "first message",
		},
	})

	// First message should succeed.
	hub.HandleMessageForTest(c, raw)
	drainChanTimeout(send, 50*time.Millisecond)

	// Second message should be rate limited by slow mode.
	raw2, _ := json.Marshal(map[string]any{
		"type": "chat_send",
		"payload": map[string]any{
			"channel_id": chID,
			"content":    "second message",
		},
	})
	hub.HandleMessageForTest(c, raw2)

	code := drainForErrorCode(send, 200*time.Millisecond)
	if code != "SLOW_MODE" {
		t.Errorf("error code = %q, want SLOW_MODE", code)
	}
}

// ─── handleChatEdit more paths (handlers.go:249 — 89.7%) ────────────────────

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

	raw, _ := json.Marshal(map[string]any{
		"type":    "chat_edit",
		"payload": "not-an-object",
	})
	hub.HandleMessageForTest(c, raw)

	code := drainForErrorCode(send, 200*time.Millisecond)
	if code != "BAD_REQUEST" {
		t.Errorf("error code = %q, want BAD_REQUEST", code)
	}
}

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

	raw, _ := json.Marshal(map[string]any{
		"type": "chat_edit",
		"payload": map[string]any{
			"message_id": -1,
			"content":    "updated",
		},
	})
	hub.HandleMessageForTest(c, raw)

	code := drainForErrorCode(send, 200*time.Millisecond)
	if code != "BAD_REQUEST" {
		t.Errorf("error code = %q, want BAD_REQUEST", code)
	}
}

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

	raw, _ := json.Marshal(map[string]any{
		"type": "chat_edit",
		"payload": map[string]any{
			"message_id": 1,
			"content":    "",
		},
	})
	hub.HandleMessageForTest(c, raw)

	code := drainForErrorCode(send, 200*time.Millisecond)
	if code != "BAD_REQUEST" {
		t.Errorf("error code = %q, want BAD_REQUEST", code)
	}
}

// ─── handleChatDelete more paths (handlers.go:298) ───────────────────────────

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

	raw, _ := json.Marshal(map[string]any{
		"type":    "chat_delete",
		"payload": "not-an-object",
	})
	hub.HandleMessageForTest(c, raw)

	code := drainForErrorCode(send, 200*time.Millisecond)
	if code != "BAD_REQUEST" {
		t.Errorf("error code = %q, want BAD_REQUEST", code)
	}
}

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

	raw, _ := json.Marshal(map[string]any{
		"type": "chat_delete",
		"payload": map[string]any{
			"message_id": -1,
		},
	})
	hub.HandleMessageForTest(c, raw)

	code := drainForErrorCode(send, 200*time.Millisecond)
	if code != "BAD_REQUEST" {
		t.Errorf("error code = %q, want BAD_REQUEST", code)
	}
}

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

	raw, _ := json.Marshal(map[string]any{
		"type": "chat_delete",
		"payload": map[string]any{
			"message_id": 99999,
		},
	})
	hub.HandleMessageForTest(c, raw)

	// Handler returns FORBIDDEN (not NOT_FOUND) to prevent message-ID enumeration.
	code := drainForErrorCode(send, 200*time.Millisecond)
	if code != "FORBIDDEN" {
		t.Errorf("error code = %q, want FORBIDDEN", code)
	}
}

// ─── handleReaction more paths (handlers.go:337) ─────────────────────────────

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

	raw, _ := json.Marshal(map[string]any{
		"type":    "reaction_add",
		"payload": "not-an-object",
	})
	hub.HandleMessageForTest(c, raw)

	code := drainForErrorCode(send, 200*time.Millisecond)
	if code != "BAD_REQUEST" {
		t.Errorf("error code = %q, want BAD_REQUEST", code)
	}
}

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

	raw, _ := json.Marshal(map[string]any{
		"type": "reaction_add",
		"payload": map[string]any{
			"message_id": 1,
			"emoji":      "",
		},
	})
	hub.HandleMessageForTest(c, raw)

	code := drainForErrorCode(send, 200*time.Millisecond)
	if code != "BAD_REQUEST" {
		t.Errorf("error code = %q, want BAD_REQUEST", code)
	}
}

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

	raw, _ := json.Marshal(map[string]any{
		"type": "reaction_add",
		"payload": map[string]any{
			"message_id": 1,
			"emoji":      strings.Repeat("x", 33),
		},
	})
	hub.HandleMessageForTest(c, raw)

	code := drainForErrorCode(send, 200*time.Millisecond)
	if code != "BAD_REQUEST" {
		t.Errorf("error code = %q, want BAD_REQUEST", code)
	}
}

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

	raw, _ := json.Marshal(map[string]any{
		"type": "reaction_add",
		"payload": map[string]any{
			"message_id": 1,
			"emoji":      "\x00bad",
		},
	})
	hub.HandleMessageForTest(c, raw)

	code := drainForErrorCode(send, 200*time.Millisecond)
	if code != "BAD_REQUEST" {
		t.Errorf("error code = %q, want BAD_REQUEST for control char emoji", code)
	}
}

// ─── handleChannelFocus with message marking (handlers.go:507) ───────────────

func TestHandleChannelFocus_UpdatesReadState(t *testing.T) {
	hub, database := newCoverageHub(t)
	user := seedCoverageOwner(t, database, "cf-readstate-user")
	chID := seedTestChannel(t, database, "cf-readstate-chan")

	// Insert a message so there's a latest_message_id.
	_, err := database.CreateMessage(context.Background(), chID, user.ID, "test message", nil)
	if err != nil {
		t.Fatalf("CreateMessage: %v", err)
	}

	send := make(chan []byte, 16)
	c := ws.NewTestClientWithUser(hub, user, 0, send)
	hub.Register(c)
	waitRegistered(t, hub, c)

	raw, _ := json.Marshal(map[string]any{
		"type": "channel_focus",
		"payload": map[string]any{
			"channel_id": chID,
		},
	})
	hub.HandleMessageForTest(c, raw)

	// No error should be sent for a valid channel_focus with existing message.
	code := drainForErrorCode(send, 100*time.Millisecond)
	if code != "" {
		t.Fatalf("expected no error for valid channel_focus, got code=%q", code)
	}
}

func TestHandleChatSend_WithNilAvatar(t *testing.T) {
	hub, database := newCoverageHub(t)
	user := seedCoverageOwner(t, database, "nil-avatar-user")
	chID := seedTestChannel(t, database, "nil-avatar-chan")
	send := make(chan []byte, 32)
	c := ws.NewTestClientWithUser(hub, user, chID, send)
	hub.Register(c)
	waitRegistered(t, hub, c)

	raw, _ := json.Marshal(map[string]any{
		"type": "chat_send",
		"id":   "avatar-req",
		"payload": map[string]any{
			"channel_id": chID,
			"content":    "hello from nil avatar user",
		},
	})
	hub.HandleMessageForTest(c, raw)

	msgs := drainChanTimeout(send, 300*time.Millisecond)
	found := false
	for _, msg := range msgs {
		var env map[string]any
		if json.Unmarshal(msg, &env) == nil && env["type"] == "chat_send_ok" {
			found = true
			break
		}
	}
	if !found {
		t.Error("expected chat_send_ok for nil-avatar user")
	}
}

func TestHandleChatSend_WithNonNilAvatar(t *testing.T) {
	hub, database := newCoverageHub(t)
	user := seedCoverageOwner(t, database, "avatar-user")
	// Set a non-nil avatar on the user.
	_, err := database.ExecContext(context.Background(), "UPDATE users SET avatar = 'https://example.com/avatar.png' WHERE id = ?", user.ID)
	if err != nil {
		t.Fatalf("UPDATE avatar: %v", err)
	}
	// Reload user to get updated avatar.
	user, err = database.GetUserByUsername(context.Background(), "avatar-user")
	if err != nil || user == nil {
		t.Fatalf("GetUserByUsername: %v", err)
	}

	chID := seedTestChannel(t, database, "avatar-chan")
	send := make(chan []byte, 32)
	c := ws.NewTestClientWithUser(hub, user, chID, send)
	hub.Register(c)
	waitRegistered(t, hub, c)

	raw, _ := json.Marshal(map[string]any{
		"type": "chat_send",
		"id":   "avatar-req2",
		"payload": map[string]any{
			"channel_id": chID,
			"content":    "hello from avatar user",
		},
	})
	hub.HandleMessageForTest(c, raw)

	msgs := drainChanTimeout(send, 300*time.Millisecond)
	foundOK := false
	foundBroadcast := false
	for _, msg := range msgs {
		var env map[string]any
		if json.Unmarshal(msg, &env) == nil {
			if env["type"] == "chat_send_ok" {
				foundOK = true
			}
			if env["type"] == "chat_message" {
				// Verify avatar is present in broadcast.
				if p, ok := env["payload"].(map[string]any); ok {
					if u, ok := p["user"].(map[string]any); ok {
						if u["avatar"] == "https://example.com/avatar.png" {
							foundBroadcast = true
						}
					}
				}
			}
		}
	}
	if !foundOK {
		t.Error("expected chat_send_ok for avatar user")
	}
	if !foundBroadcast {
		t.Error("expected chat_message with non-nil avatar")
	}
}
