package channeladmin

import (
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
)

// TestValidateVisibilityStrict locks in I3: the raw Agent API is not protected
// by the Desktop JSONSchema, so the service must accept only private/public
// (case-insensitive) and reject anything else instead of silently coercing.
func TestValidateVisibilityStrict(t *testing.T) {
	cases := []struct {
		in      string
		want    string
		wantErr bool
	}{
		{"", "private", false},
		{"private", "private", false},
		{"PRIVATE", "private", false},
		{"public", "public", false},
		{"Public", "public", false},
		{"workspace", "", true},
		{"xyz", "", true},
		{" public ", "public", false},
	}
	for _, tc := range cases {
		got, err := validateVisibility(tc.in)
		if tc.wantErr {
			require.Error(t, err, "input %q", tc.in)
			continue
		}
		require.NoError(t, err, "input %q", tc.in)
		require.Equal(t, tc.want, got, "input %q", tc.in)
	}
}

// TestValidateRequestIDShape locks in I3: requestId is required and bounded.
func TestValidateRequestIDShape(t *testing.T) {
	require.NoError(t, validateRequestID("req-1"))
	require.Error(t, validateRequestID(""))
	require.Error(t, validateRequestID("   "))
	require.Error(t, validateRequestID(strings.Repeat("a", 129)))
	require.NoError(t, validateRequestID(strings.Repeat("a", 128)))
}

// TestValidateDisplayName locks in I3/C: an empty or oversized name must be a
// stable validation error before it reaches the store.
func TestValidateDisplayName(t *testing.T) {
	require.Error(t, validateDisplayName(""))
	require.Error(t, validateDisplayName("   "))
	require.NoError(t, validateDisplayName("release-room"))
	require.Error(t, validateDisplayName(strings.Repeat("a", 256)))
	require.NoError(t, validateDisplayName(strings.Repeat("a", 255)))
	// Rune count, not bytes: 200 Chinese characters are 600 UTF-8 bytes but a
	// valid varchar(255) name and must pass.
	require.NoError(t, validateDisplayName(strings.Repeat("频", 200)))
	require.Error(t, validateDisplayName(strings.Repeat("频", 256)))
}

// TestDigestCreateDescriptionPresence locks in I2: the create digest must
// distinguish an absent description from an explicit empty description so a
// replay with a different payload cannot silently succeed.
func TestDigestCreateDescriptionPresence(t *testing.T) {
	base := CreateInput{RequestID: "r", DisplayName: "room"}
	nilDesc := base
	emptyDesc := base
	emptyDesc.Description = strPtr("")
	otherDesc := base
	otherDesc.Description = strPtr("notes")

	require.False(t, equalDigest(digestCreate(nilDesc), digestCreate(emptyDesc)),
		"nil description must differ from explicit empty")
	require.False(t, equalDigest(digestCreate(nilDesc), digestCreate(otherDesc)))
	require.False(t, equalDigest(digestCreate(emptyDesc), digestCreate(otherDesc)))
	// Deterministic: same input reproduces the same digest.
	require.True(t, equalDigest(digestCreate(nilDesc), digestCreate(base)))
}

// TestDigestUpdateVisibilityNormalized locks in I2: equivalent visibility
// values (PUBLIC vs public) must produce the same digest, while nil vs an
// explicit value must differ.
func TestDigestUpdateVisibilityNormalized(t *testing.T) {
	base := func(vis *string) UpdateInput {
		return UpdateInput{RequestID: "r", ChannelID: "chn_1", ExpectedVersion: 1, Visibility: vis}
	}
	upper := base(strPtr("PUBLIC"))
	lower := base(strPtr("public"))
	require.True(t, equalDigest(digestUpdate(upper), digestUpdate(lower)),
		"PUBLIC and public must normalize to the same digest")

	nilVis := base(nil)
	setVis := base(strPtr("public"))
	require.False(t, equalDigest(digestUpdate(nilVis), digestUpdate(setVis)),
		"nil visibility must differ from an explicit value")
}

// TestDigestUpdateFieldPresence locks in I2 for *string patch fields: nil vs
// empty string must produce different digests.
func TestDigestUpdateFieldPresence(t *testing.T) {
	nilDesc := UpdateInput{RequestID: "r", ChannelID: "chn_1", ExpectedVersion: 1}
	emptyDesc := UpdateInput{RequestID: "r", ChannelID: "chn_1", ExpectedVersion: 1, Description: strPtr("")}
	require.False(t, equalDigest(digestUpdate(nilDesc), digestUpdate(emptyDesc)))
}

// TestTrimmedStringPointerPreservesPresence locks in the pointer-preserving
// helper used by the digest.
func TestTrimmedStringPointerPreservesPresence(t *testing.T) {
	require.Nil(t, trimmedStringPointer(nil))
	empty := trimmedStringPointer(strPtr(""))
	require.NotNil(t, empty)
	require.Equal(t, "", *empty)
	trimmed := trimmedStringPointer(strPtr("  hi "))
	require.Equal(t, "hi", *trimmed)
}

// TestNormalizedVisibilityPointerPreservesPresence locks in I2 + the visibility
// digest normalization together.
func TestNormalizedVisibilityPointerPreservesPresence(t *testing.T) {
	require.Nil(t, normalizedVisibilityPointer(nil))
	v := normalizedVisibilityPointer(strPtr("PUBLIC"))
	require.NotNil(t, v)
	require.Equal(t, "public", *v)
}

// TestEqualDigest is a sanity check for the constant-time-shaped comparison.
func TestEqualDigest(t *testing.T) {
	a := []byte{1, 2, 3}
	require.True(t, equalDigest(a, a))
	require.False(t, equalDigest(a, []byte{1, 2, 4}))
	require.False(t, equalDigest(a, []byte{1, 2}))
}

func strPtr(value string) *string { return &value }
