package token

import (
	"strings"
	"testing"
	"time"

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

func TestCodecOpensAcrossReplicasWithExactBinding(t *testing.T) {
	t.Parallel()

	now := time.Date(2026, 8, 15, 12, 0, 0, 0, time.UTC)
	keys := Keyring{Current: Key{
		ID:       "k1",
		Material: []byte(strings.Repeat("a", 32)),
	}}
	first, err := New(keys, "production", func() time.Time { return now })
	require.NoError(t, err)
	second, err := New(keys, "production", func() time.Time {
		return now.Add(time.Minute)
	})
	require.NoError(t, err)
	binding := validBinding()
	encoded, err := first.Seal(
		Claims{
			Kind:           binding.Kind,
			WorkspaceID:    binding.WorkspaceID,
			InstallationID: binding.InstallationID,
			ConnectionID:   binding.ConnectionID,
			Generation:     binding.Generation,
			AgentID:        binding.AgentID,
			Action:         binding.Action,
			Mailbox:        "INBOX",
			UIDValidity:    42,
			UID:            7,
			Revision:       9,
		},
		5*time.Minute,
	)
	require.NoError(t, err)
	require.NotContains(t, encoded, "INBOX")
	require.NotContains(t, encoded, "wsp_1")
	require.NotContains(t, encoded, "provider-message-id")

	opened, err := second.Open(encoded, binding)
	require.NoError(t, err)
	require.Equal(t, uint32(7), opened.UID)
	require.Equal(t, uint64(9), opened.Revision)
}

func TestCodecRejectsEveryBindingMismatch(t *testing.T) {
	t.Parallel()

	now := time.Date(2026, 8, 15, 12, 0, 0, 0, time.UTC)
	codec := newTestCodec(t, now)
	binding := validBinding()
	encoded, err := codec.Seal(
		validClaims(binding),
		5*time.Minute,
	)
	require.NoError(t, err)
	tests := []struct {
		name   string
		mutate func(*Binding)
	}{
		{"kind", func(value *Binding) { value.Kind = KindDraft }},
		{"workspace", func(value *Binding) { value.WorkspaceID = "wsp_2" }},
		{"installation", func(value *Binding) { value.InstallationID = "appinst_2" }},
		{"connection", func(value *Binding) { value.ConnectionID = "conn_2" }},
		{"generation", func(value *Binding) { value.Generation = "sha256:other" }},
		{"agent", func(value *Binding) { value.AgentID = "agt_2" }},
		{"action", func(value *Binding) { value.Action = "get-message-other" }},
	}
	for _, test := range tests {
		test := test
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()
			other := binding
			test.mutate(&other)
			_, openErr := codec.Open(encoded, other)
			require.EqualError(t, openErr, "Mail token is invalid or expired")
		})
	}
}

func TestCodecRejectsTamperWrongAudienceAndExpiry(t *testing.T) {
	t.Parallel()

	now := time.Date(2026, 8, 15, 12, 0, 0, 0, time.UTC)
	codec := newTestCodec(t, now)
	binding := validBinding()
	encoded, err := codec.Seal(validClaims(binding), time.Minute)
	require.NoError(t, err)

	parts := strings.Split(encoded, ".")
	require.Len(t, parts, 4)
	replacement := byte('A')
	if parts[3][0] == replacement {
		replacement = 'B'
	}
	parts[3] = string(replacement) + parts[3][1:]
	tampered := strings.Join(parts, ".")
	_, err = codec.Open(tampered, binding)
	require.Error(t, err)

	wrongAudience, err := New(
		Keyring{Current: Key{
			ID:       "k1",
			Material: []byte(strings.Repeat("a", 32)),
		}},
		"staging",
		func() time.Time { return now },
	)
	require.NoError(t, err)
	_, err = wrongAudience.Open(encoded, binding)
	require.Error(t, err)

	expired := newTestCodec(t, now.Add(2*time.Minute))
	_, err = expired.Open(encoded, binding)
	require.ErrorContains(t, err, "invalid or expired")
}

func TestCodecAcceptsConfiguredLegacyKeyButMintsCurrentKey(t *testing.T) {
	t.Parallel()

	now := time.Date(2026, 8, 15, 12, 0, 0, 0, time.UTC)
	oldCodec, err := New(
		Keyring{Current: Key{
			ID:       "old",
			Material: []byte(strings.Repeat("o", 32)),
		}},
		"production",
		func() time.Time { return now },
	)
	require.NoError(t, err)
	binding := validBinding()
	oldToken, err := oldCodec.Seal(validClaims(binding), 5*time.Minute)
	require.NoError(t, err)

	rotated, err := New(
		Keyring{
			Current: Key{
				ID:       "new",
				Material: []byte(strings.Repeat("n", 32)),
			},
			Legacy: []Key{{
				ID:       "old",
				Material: []byte(strings.Repeat("o", 32)),
			}},
		},
		"production",
		func() time.Time { return now.Add(time.Minute) },
	)
	require.NoError(t, err)
	_, err = rotated.Open(oldToken, binding)
	require.NoError(t, err)
	newToken, err := rotated.Seal(validClaims(binding), 5*time.Minute)
	require.NoError(t, err)
	require.Contains(t, newToken, ".new.")
}

func TestCodecBindsCursorFilterAndLimit(t *testing.T) {
	t.Parallel()

	now := time.Date(2026, 8, 15, 12, 0, 0, 0, time.UTC)
	codec := newTestCodec(t, now)
	binding := validBinding()
	binding.Kind = KindCursor
	encoded, err := codec.Seal(
		Claims{
			Kind:           binding.Kind,
			WorkspaceID:    binding.WorkspaceID,
			InstallationID: binding.InstallationID,
			ConnectionID:   binding.ConnectionID,
			Generation:     binding.Generation,
			AgentID:        binding.AgentID,
			Action:         binding.Action,
			Cursor:         "provider-cursor",
			FilterDigest:   "sha256:" + strings.Repeat("f", 64),
			Limit:          20,
		},
		time.Minute,
	)
	require.NoError(t, err)
	claims, err := codec.Open(encoded, binding)
	require.NoError(t, err)
	require.Equal(t, 20, claims.Limit)
	require.Equal(t, "provider-cursor", claims.Cursor)
}

func newTestCodec(t *testing.T, now time.Time) *Codec {
	t.Helper()
	codec, err := New(
		Keyring{Current: Key{
			ID:       "k1",
			Material: []byte(strings.Repeat("a", 32)),
		}},
		"production",
		func() time.Time { return now },
	)
	require.NoError(t, err)
	return codec
}

func validBinding() Binding {
	return Binding{
		Kind:           KindMessage,
		WorkspaceID:    "wsp_1",
		InstallationID: "appinst_1",
		ConnectionID:   "conn_1",
		Generation:     "sha256:" + strings.Repeat("g", 64),
		AgentID:        "agt_1",
		Action:         "get-message",
	}
}

func validClaims(binding Binding) Claims {
	return Claims{
		Kind:           binding.Kind,
		WorkspaceID:    binding.WorkspaceID,
		InstallationID: binding.InstallationID,
		ConnectionID:   binding.ConnectionID,
		Generation:     binding.Generation,
		AgentID:        binding.AgentID,
		Action:         binding.Action,
		Mailbox:        "INBOX",
		UIDValidity:    42,
		UID:            7,
		Revision:       9,
	}
}
