package app

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/api/testkit/httputil"
	storeapp "github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

func TestBindAppNotificationJSONRejectsOversizedBody(t *testing.T) {
	recorder := httptest.NewRecorder()
	ctx, _ := gin.CreateTestContext(recorder)
	ctx.Request = httptest.NewRequest(http.MethodPatch, "/notifications/btn_1", strings.NewReader(`{"payload":"`+strings.Repeat("x", 1024)+`"}`))
	ctx.Request.Header.Set("Content-Type", "application/json")

	var destination map[string]any
	require.False(t, bindAppNotificationJSON(ctx, 128, &destination))
	require.Equal(t, http.StatusRequestEntityTooLarge, recorder.Code)
}

func TestBindAppNotificationJSONPreservesLargeIntegers(t *testing.T) {
	recorder := httptest.NewRecorder()
	ctx, _ := gin.CreateTestContext(recorder)
	ctx.Request = httptest.NewRequest(
		http.MethodPost,
		"/messages/msg_1/app-notification-actions/approve",
		strings.NewReader(`{"cardRevision":3,"clientActionId":"action-1","input":{"sequence":9007199254740993}}`),
	)
	ctx.Request.Header.Set("Content-Type", "application/json")

	var request struct {
		Input map[string]any `json:"input"`
	}
	require.True(t, bindAppNotificationJSON(ctx, appNotificationActionBodyLimit, &request))
	sequence, ok := request.Input["sequence"].(json.Number)
	require.True(t, ok)
	require.Equal(t, "9007199254740993", sequence.String())
}

func TestBindAppNotificationJSONRejectsTrailingDocument(t *testing.T) {
	recorder := httptest.NewRecorder()
	ctx, _ := gin.CreateTestContext(recorder)
	ctx.Request = httptest.NewRequest(
		http.MethodPost,
		"/messages/msg_1/app-notification-actions/approve",
		strings.NewReader(`{"cardRevision":3} {"clientActionId":"action-1"}`),
	)
	ctx.Request.Header.Set("Content-Type", "application/json")

	var request map[string]any
	require.False(t, bindAppNotificationJSON(ctx, appNotificationActionBodyLimit, &request))
	require.Equal(t, http.StatusBadRequest, recorder.Code)
}

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

	fake := newFakeStore()
	router := newAppRouter(Dependencies{AppStore: fake})
	response := httputil.DoRaw(t, router, http.MethodPatch,
		"/internal/workspaces/wsp_1/app-notifications/btn_1",
		http.Header{
			"Authorization": {"Internal internal-secret"},
			"Content-Type":  {"application/json"},
		}, oversizedAppNotificationMutationBody())

	require.Equal(t, http.StatusRequestEntityTooLarge, response.Code)
	require.Equal(t, 1, fake.appNotificationInternalAuthCalls)
	require.Equal(t, "wsp_1", fake.lastAppNotificationInternalWorkspaceID)
	require.Equal(t, "internal-secret", fake.lastAppNotificationInternalCredential)
	require.Zero(t, fake.appNotificationInternalMutationCalls)
}

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

	tests := []struct {
		name          string
		authorization string
		authErr       error
		wantAuthCalls int
	}{
		{name: "missing credential"},
		{name: "wrong credential scheme", authorization: "Bearer wrong"},
		{name: "invalid registered credential", authorization: "Internal wrong", authErr: storeerr.ErrUnauthorized, wantAuthCalls: 1},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			fake := newFakeStore()
			fake.appNotificationInternalAuthErr = test.authErr
			router := newAppRouter(Dependencies{AppStore: fake})
			headers := http.Header{"Content-Type": {"application/json"}}
			if test.authorization != "" {
				headers.Set("Authorization", test.authorization)
			}
			response := httputil.DoRaw(t, router, http.MethodPatch,
				"/internal/workspaces/wsp_1/app-notifications/btn_1", headers,
				oversizedAppNotificationMutationBody())

			require.Equal(t, http.StatusUnauthorized, response.Code, "authentication must win over the body-size error")
			require.Equal(t, test.wantAuthCalls, fake.appNotificationInternalAuthCalls)
			require.Zero(t, fake.appNotificationInternalMutationCalls)
		})
	}
}

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

	fake := newFakeStore()
	fake.authenticated.User.ID = "usr_session"
	router := newAppRouter(Dependencies{AppStore: fake})
	response := httputil.DoJSON(t, router, http.MethodPost,
		"/api/v1/workspaces/wsp_1/messages/msg_1/app-notification-actions/approve",
		map[string]any{
			"cardRevision":   3,
			"clientActionId": "action-1",
			"input":          map[string]any{"comment": "approved"},
			"actorMemberId":  "mem_spoofed",
			"actorUserId":    "usr_spoofed",
		})

	require.Equal(t, http.StatusOK, response.Code)
	require.Equal(t, 1, fake.appNotificationActionCalls)
	require.Equal(t, "usr_session", fake.lastAppNotificationActionUserID)
	require.Equal(t, "wsp_1", fake.lastAppNotificationActionWorkspaceID)
	require.Equal(t, "msg_1", fake.lastAppNotificationActionCommand.MessageID)
	require.Equal(t, "approve", fake.lastAppNotificationActionCommand.ActionKey)
	require.Equal(t, int64(3), fake.lastAppNotificationActionCommand.CardRevision)
	require.Equal(t, "action-1", fake.lastAppNotificationActionCommand.ClientActionID)
	require.Empty(t, fake.lastAppNotificationActionCommand.ActorMemberID, "the store resolves the Human member exclusively from the authenticated session user")
}

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

	now := time.Date(2026, time.July, 11, 1, 2, 3, 0, time.UTC)
	fake := newFakeStore()
	fake.authenticated.User.ID = "usr_session"
	fake.appNotificationCard = storeapp.AppNotificationCardView{
		NotificationID:       "botn_1",
		NotificationRevision: 2,
		NotificationStatus:   "active",
		MessageID:            "msg_1",
		ScopeID:              "dm_1",
		SchemaVersion:        "1",
		CardRevision:         3,
		RenderPayloadJSON:    json.RawMessage(`{"schemaVersion":"1","title":"Approval"}`),
		State:                "active",
		CreatedAt:            now,
		UpdatedAt:            now,
	}
	router := newAppRouter(Dependencies{AppStore: fake})
	response := httputil.DoJSON(t, router, http.MethodGet,
		"/api/v1/workspaces/wsp_1/messages/msg_1/app-notification-card", nil)

	require.Equal(t, http.StatusOK, response.Code)
	require.Equal(t, 1, fake.appNotificationCardCalls)
	require.Equal(t, "usr_session", fake.lastAppNotificationCardUserID)
	require.Equal(t, "wsp_1", fake.lastAppNotificationCardWorkspaceID)
	require.Equal(t, "msg_1", fake.lastAppNotificationCardMessageID)
	var body dto.AppNotificationCardResponse
	require.NoError(t, json.Unmarshal(response.Body.Bytes(), &body))
	require.Equal(t, "botn_1", body.NotificationID)
	require.Equal(t, int64(3), body.CardRevision)
	require.JSONEq(t, `{"schemaVersion":"1","title":"Approval"}`, string(body.RenderPayload))
}

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

	fake := newFakeStore()
	router := newAppRouter(Dependencies{AppStore: fake})
	response := httputil.DoRaw(t, router, http.MethodPost,
		"/api/v1/workspaces/wsp_1/messages/msg_1/app-notification-actions/approve",
		http.Header{
			"Authorization":  {"Bearer token"},
			"X-Workspace-Id": {"wsp_1"},
			"Content-Type":   {"application/json"},
		},
		[]byte(`{"cardRevision":3,"clientActionId":"action-1","input":{"comment":"`+
			strings.Repeat("x", int(appNotificationActionBodyLimit)+1024)+`"}}`),
	)

	require.Equal(t, http.StatusRequestEntityTooLarge, response.Code)
	require.Zero(t, fake.appNotificationActionCalls)
}

func oversizedAppNotificationMutationBody() []byte {
	return []byte(`{"expectedRevision":1,"clientMutationId":"` + strings.Repeat("x", int(appNotificationRequestBodyLimit)+1024) + `"}`)
}
