package model

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"testing"
	"time"

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

func TestUnwrapTiDBOpaqueJSONText(t *testing.T) {
	usage := []byte(`{"input_tokens":10}`)
	for _, typeCode := range []int{15, 249, 250, 251, 252, 253, 254} {
		t.Run(fmt.Sprintf("type%d", typeCode), func(t *testing.T) {
			wrapped, err := json.Marshal(fmt.Sprintf(
				"base64:type%d:%s",
				typeCode,
				base64.StdEncoding.EncodeToString(usage),
			))
			require.NoError(t, err)

			decoded, err := UnwrapTiDBOpaqueJSONText("Knowledge Q&A existing usage", wrapped)
			require.NoError(t, err)
			require.JSONEq(t, string(usage), string(decoded))
		})
	}
}

func TestUnwrapTiDBOpaqueJSONTextPreservesOrdinaryJSON(t *testing.T) {
	raw := []byte(`{"input_tokens":10}`)
	decoded, err := UnwrapTiDBOpaqueJSONText("Knowledge Q&A existing usage", raw)
	require.NoError(t, err)
	require.Equal(t, raw, decoded)
}

func TestUnwrapTiDBOpaqueJSONTextRejectsMalformedEnvelope(t *testing.T) {
	tests := []struct {
		name    string
		encoded string
		want    string
	}{
		{name: "unsupported type", encoded: "base64:type3:MQ==", want: "unsupported field type"},
		{name: "invalid base64", encoded: "base64:type253:not-base64", want: "invalid base64 payload"},
		{name: "invalid JSON", encoded: "base64:type253:" + base64.StdEncoding.EncodeToString([]byte("not-json")), want: "invalid JSON payload"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			raw, err := json.Marshal(test.encoded)
			require.NoError(t, err)
			_, err = UnwrapTiDBOpaqueJSONText("Knowledge Q&A existing usage", raw)
			require.ErrorContains(t, err, test.want)
			require.NotContains(t, err.Error(), test.encoded)
		})
	}
}

func TestJSONDocumentsEqualPreservesIntegerPrecision(t *testing.T) {
	require.True(t, JSONDocumentsEqual(
		[]byte(`{"version":1,"mode":"service_account"}`),
		[]byte("{\n  \"mode\": \"service_account\", \"version\": 1\n}"),
	))
	require.False(t, JSONDocumentsEqual(
		[]byte(`{"sequence":9007199254740992}`),
		[]byte(`{"sequence":9007199254740993}`),
	))
	require.True(t, JSONDocumentsEqual([]byte(`legacy`), []byte(` legacy `)))
	require.False(t, JSONDocumentsEqual([]byte(`legacy`), []byte(`other`)))
}

func TestConvertToJSONMapReturnsDetachedObject(t *testing.T) {
	source := map[string]any{
		"kind":  "task",
		"count": 2,
	}
	converted, err := ConvertToJSONMap(source)
	require.NoError(t, err)
	require.Equal(t, source, converted)

	converted["kind"] = "channel"
	require.Equal(t, "task", source["kind"])

	converted, err = ConvertToJSONMap(struct {
		Kind  string `json:"kind"`
		Count int    `json:"count"`
	}{Kind: "task", Count: 2})
	require.NoError(t, err)
	require.Equal(t, map[string]any{
		"kind":  "task",
		"count": float64(2),
	}, converted)
}

func TestUnmarshalTaskConversationProjectionAllowsAdditionalProjectionFields(t *testing.T) {
	projection, err := UnmarshalTaskConversationProjection([]byte(
		`{"kind":"task","preview":"Latest update","lastAuthorName":"Ada","subscriptionState":"active"}`,
	))
	require.NoError(t, err)
	require.Equal(t, TaskConversationProjectionJSON{
		Preview:        "Latest update",
		LastAuthorName: "Ada",
	}, projection)

	_, err = UnmarshalTaskConversationProjection([]byte(`{"preview":`))
	require.ErrorContains(t, err, "decode task conversation projection")
}

func TestTaskSnapshotShadowV1CodecPreservesWireShape(t *testing.T) {
	value := TaskSnapshotShadowV1JSON{
		ScopeType:                  "task",
		MessageUnreadCount:         1,
		ActivityUnreadCount:        2,
		ManualUnread:               true,
		LastMessageReadSeq:         3,
		LastActivityReadSeq:        4,
		EffectiveFromSeq:           5,
		LatestMessageAccountedSeq:  6,
		LatestActivityAccountedSeq: 7,
		NotificationState:          "active",
		MembershipVisibility:       "public",
		ScopeMessageGeneration:     8,
		ScopeActivityGeneration:    9,
		MembershipGeneration:       10,
		NotificationGeneration:     11,
		ManualUnreadGeneration:     12,
		RewindGeneration:           13,
		UpdatedAt:                  time.Date(2026, 7, 29, 6, 7, 8, 123456000, time.UTC),
	}

	payload, err := MarshalTaskSnapshotShadowV1(value)
	require.NoError(t, err)
	require.Equal(
		t,
		`{"scopeType":"task","messageUnreadCount":1,"activityUnreadCount":2,"manualUnread":true,"lastMessageReadSeq":3,"lastActivityReadSeq":4,"effectiveFromSeq":5,"latestMessageAccountedSeq":6,"latestActivityAccountedSeq":7,"notificationState":"active","membershipVisibility":"public","scopeMessageGeneration":8,"scopeActivityGeneration":9,"membershipGeneration":10,"notificationGeneration":11,"manualUnreadGeneration":12,"rewindGeneration":13,"updatedAt":"2026-07-29T06:07:08.123456Z"}`,
		string(payload),
	)

	decoded, err := UnmarshalTaskSnapshotShadowV1(append(payload, '\n'))
	require.NoError(t, err)
	require.Equal(t, value, decoded)
}

func TestUnmarshalTaskSnapshotShadowV1IsStrictAndRequiresEOF(t *testing.T) {
	tests := []struct {
		name    string
		payload string
		wantErr string
	}{
		{
			name:    "unknown field",
			payload: `{"scopeType":"task","future":true}`,
			wantErr: `unknown field "future"`,
		},
		{
			name:    "trailing value",
			payload: `{"scopeType":"task"} {}`,
			wantErr: "trailing JSON value",
		},
		{
			name:    "malformed trailing content",
			payload: `{"scopeType":"task"} trailing`,
			wantErr: "invalid character",
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			_, err := UnmarshalTaskSnapshotShadowV1([]byte(test.payload))
			require.ErrorContains(t, err, test.wantErr)
			require.ErrorContains(t, err, "decode Task unread snapshot shadow")
		})
	}
}
