package privateagent

import (
	"encoding/base64"
	"encoding/json"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/store/message"
)

// encodeCursorJSON is a test-only encoder that builds an opaque cursor string
// from a raw map. It mirrors the wire shape used by the production encoder but
// lets tests craft malformed payloads (unknown fields, wrong versions, etc.)
// without going through the strict production path.
func encodeCursorJSON(t *testing.T, payload map[string]any) string {
	t.Helper()
	raw, err := json.Marshal(payload)
	require.NoError(t, err)
	return base64.RawURLEncoding.EncodeToString(raw)
}

// encodeRawJSON wraps an arbitrary JSON literal as an opaque cursor string.
func encodeRawJSON(t *testing.T, raw string) string {
	t.Helper()
	return base64.RawURLEncoding.EncodeToString([]byte(raw))
}

// encodeCursorJSONConcat serializes a primary payload, concatenates one or
// more secondary payloads after it as raw JSON, then base64-encodes the whole
// string. This is the only way to exercise the decoder's top-level trailing
// token check: appending raw JSON after a base64-encoded cursor would be
// rejected as bad base64 before reaching the trailing-token check.
func encodeCursorJSONConcat(t *testing.T, primary map[string]any, extras ...map[string]any) string {
	t.Helper()
	raw, err := json.Marshal(primary)
	require.NoError(t, err)
	for _, extra := range extras {
		chunk, err := json.Marshal(extra)
		require.NoError(t, err)
		raw = append(raw, chunk...)
	}
	return base64.RawURLEncoding.EncodeToString(raw)
}

func TestEncodeScopeNotificationAgentCursorRoundTrip(t *testing.T) {
	key := scopeNotificationAgentCursorKey{DisplayName: "alice-app", AgentID: "agt_1"}
	encoded := encodeScopeNotificationAgentCursor(key)
	require.NotEmpty(t, encoded)

	decoded, err := decodeScopeNotificationAgentCursor(encoded)
	require.NoError(t, err)
	require.Equal(t, key, decoded)
}

func TestEncodeScopeNotificationAgentCursorOmitsEmpty(t *testing.T) {
	require.Empty(t, encodeScopeNotificationAgentCursor(scopeNotificationAgentCursorKey{}))
}

func TestDecodeScopeNotificationAgentCursorAcceptsBlank(t *testing.T) {
	key, err := decodeScopeNotificationAgentCursor("   ")
	require.NoError(t, err)
	require.Equal(t, scopeNotificationAgentCursorKey{}, key)
}

func TestDecodeScopeNotificationAgentCursorRejectsMalformed(t *testing.T) {
	cases := []struct {
		name string
		raw  string
	}{
		{"not base64", "!!!not-base64!!!"},
		{"unknown field", encodeCursorJSON(t, map[string]any{"v": 1, "d": "x", "a": "y", "extra": 1})},
		{"wrong version", encodeCursorJSON(t, map[string]any{"v": 2, "d": "x", "a": "y"})},
		{"missing version", encodeCursorJSON(t, map[string]any{"d": "x", "a": "y"})},
		{"missing name", encodeCursorJSON(t, map[string]any{"v": 1, "a": "y"})},
		{"missing agent", encodeCursorJSON(t, map[string]any{"v": 1, "d": "x"})},
		{"empty name", encodeCursorJSON(t, map[string]any{"v": 1, "d": "", "a": "y"})},
		{"empty agent", encodeCursorJSON(t, map[string]any{"v": 1, "d": "x", "a": ""})},
		// sort_key canonical form is owned by the DB. The decoder no longer
		// rejects uppercase or whitespace in `d`; it trusts whatever the
		// encoder (which reads DB LOWER(TRIM(display_name))) produced. These
		// cases are accepted today:
		//   - uppercase name
		//   - name with leading/trailing space
		// `a` is still a trimmed agent_id, so whitespace is rejected.
		{"agent with leading space", encodeCursorJSON(t, map[string]any{"v": 1, "d": "alice", "a": " y"})},
		{"agent with trailing space", encodeCursorJSON(t, map[string]any{"v": 1, "d": "alice", "a": "y "})},
		{"trailing JSON token", encodeCursorJSONConcat(t, map[string]any{"v": 1, "d": "alice", "a": "y"}, map[string]any{"z": 9})},
		{"not an object", encodeRawJSON(t, "[1,2,3]")},
		{"JSON string instead of object", encodeRawJSON(t, "\"hello\"")},
		{"JSON number", encodeRawJSON(t, "42")},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			_, err := decodeScopeNotificationAgentCursor(tc.raw)
			require.ErrorIs(t, err, errInvalidScopeNotificationAgentCursor)
		})
	}
}

func TestDecodeScopeNotificationAgentCursorRejectsOversize(t *testing.T) {
	huge := encodeCursorJSON(t, map[string]any{
		"v": 1,
		"d": strings.Repeat("a", maxScopeNotificationAgentCursorName+1),
		"a": "y",
	})
	_, err := decodeScopeNotificationAgentCursor(huge)
	require.ErrorIs(t, err, errInvalidScopeNotificationAgentCursor)
}

func TestClampScopeNotificationAgentPageSize(t *testing.T) {
	require.Equal(t, defaultScopeNotificationAgentPageSize, clampScopeNotificationAgentPageSize(0))
	require.Equal(t, defaultScopeNotificationAgentPageSize, clampScopeNotificationAgentPageSize(-1))
	require.Equal(t, defaultScopeNotificationAgentPageSize, clampScopeNotificationAgentPageSize(-100))
	require.Equal(t, 1, clampScopeNotificationAgentPageSize(1))
	require.Equal(t, maxScopeNotificationAgentPageSize, clampScopeNotificationAgentPageSize(maxScopeNotificationAgentPageSize))
	require.Equal(t, maxScopeNotificationAgentPageSize, clampScopeNotificationAgentPageSize(maxScopeNotificationAgentPageSize+1))
	require.Equal(t, maxScopeNotificationAgentPageSize, clampScopeNotificationAgentPageSize(10_000_000))
}

func TestFinalizeScopeNotificationAgentPageFirstPageHasMore(t *testing.T) {
	pageSize := 3
	rows := []message.ScopeNotificationAgentRow{
		{AgentID: "agt_1", Name: "Alice"},
		{AgentID: "agt_2", Name: "Bob"},
		{AgentID: "agt_3", Name: "Carol"},
		{AgentID: "agt_4", Name: "Dave"},
	}
	sortKeys := []string{"alice", "bob", "carol", "dave"}

	page, nextCursor, hasMore := finalizeScopeNotificationAgentPageWithSortKey(rows, sortKeys, pageSize)
	require.True(t, hasMore)
	require.Len(t, page, pageSize)
	require.Equal(t, "agt_3", page[len(page)-1].AgentID)
	require.NotEmpty(t, nextCursor)

	decoded, err := decodeScopeNotificationAgentCursor(nextCursor)
	require.NoError(t, err)
	require.Equal(t, "agt_3", decoded.AgentID)
	require.Equal(t, "carol", decoded.DisplayName)
}

func TestFinalizeScopeNotificationAgentPageLastPageNoMore(t *testing.T) {
	pageSize := 3
	rows := []message.ScopeNotificationAgentRow{
		{AgentID: "agt_1", Name: "Alice"},
		{AgentID: "agt_2", Name: "Bob"},
	}
	sortKeys := []string{"alice", "bob"}

	page, nextCursor, hasMore := finalizeScopeNotificationAgentPageWithSortKey(rows, sortKeys, pageSize)
	require.False(t, hasMore)
	require.Empty(t, nextCursor)
	require.Len(t, page, 2)
}

func TestFinalizeScopeNotificationAgentPageExactPage(t *testing.T) {
	pageSize := 3
	rows := []message.ScopeNotificationAgentRow{
		{AgentID: "agt_1", Name: "Alice"},
		{AgentID: "agt_2", Name: "Bob"},
		{AgentID: "agt_3", Name: "Carol"},
	}
	sortKeys := []string{"alice", "bob", "carol"}

	page, nextCursor, hasMore := finalizeScopeNotificationAgentPageWithSortKey(rows, sortKeys, pageSize)
	require.False(t, hasMore)
	require.Empty(t, nextCursor)
	require.Len(t, page, pageSize)
}

func TestFinalizeScopeNotificationAgentPageEmpty(t *testing.T) {
	page, nextCursor, hasMore := finalizeScopeNotificationAgentPageWithSortKey(nil, nil, 10)
	require.False(t, hasMore)
	require.Empty(t, nextCursor)
	require.Empty(t, page)
}
