package im

import (
	"context"
	"errors"
	"net/http/httptest"
	"testing"
	"time"

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

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/store/channel"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/taskboard"
)

func TestScopeSnapshotAccessJoinedAggregatesChannelScope(t *testing.T) {
	now := time.Date(2026, 5, 12, 10, 0, 0, 0, time.UTC)
	access := &fakeScopeSnapshotAccessResolver{
		resolution: channel.ChannelAccessResolution{
			Status:  channel.ChannelAccessStatusJoined,
			Channel: message.ChannelSummary{ChannelID: "chn_1", Joined: true},
		},
	}
	messages := &fakeScopeSnapshotMessageStore{page: message.AppMessagePage{
		Messages:   []message.AppMessage{{ID: "msg_1", ChannelID: "chn_1", CreatedAt: now, Seq: 7}},
		HasMore:    true,
		NextCursor: 7,
	}}
	members := &fakeScopeSnapshotChannelMemberStore{members: message.ChannelMemberList{Humans: []message.ChannelMemberSummary{{ID: "usr_1", Name: "me", DisplayName: "Me"}}}}
	tasks := &fakeScopeSnapshotTaskBoard{
		list: taskboard.ListTasksResponse{Tasks: []taskboard.Task{{ID: "tsk_1", ChannelID: "chn_1", TaskNumber: 1, Title: "Do it", Status: "todo", CreatedAt: now}}},
		capability: taskboard.ResolvePrivateAgentContextContributionCapabilityResponse{
			TargetScopeType:        "channel",
			TargetScopeID:          "chn_1",
			ExecutionOwnerMemberID: "mem_1",
			Relation:               "owner",
			CanContributeContext:   true,
			CanWake:                true,
			CanShare:               true,
		},
	}
	handler := &ScopeSnapshotHandler{access: access, messages: messages, channelMembers: members, taskBoard: tasks}

	request := imWorkspaceRequest(gin.Param{Key: "scopeId", Value: "chn_1"})
	request.Gin = ginContextWithQuery(request.Gin, "include=messages,tasks,members,privateAgentCapability&executionOwnerMemberId=mem_1")
	response, err := handler.Get(request)
	require.NoError(t, err)
	body, ok := response.Body.(scopeSnapshotResponse)
	require.True(t, ok)

	require.Equal(t, "msg_1", body.Messages.Messages[0].ID)
	require.True(t, body.Messages.HasMore)
	require.Equal(t, int64(7), body.Messages.NextCursor)
	require.Equal(t, "tsk_1", body.Tasks.Tasks[0].ID)
	require.Equal(t, "usr_1", body.Members.Humans[0].ID)
	require.Equal(t, "owner", body.PrivateAgentCapability.Relation)
	require.Equal(t, "usr_1", messages.userID)
	require.Equal(t, "wsp_1", messages.workspaceID)
	require.Equal(t, "chn_1", messages.scopeID)
	require.Equal(t, defaultScopeSnapshotMessageLimit, messages.limit)
	require.False(t, tasks.includeSubtasks)
	require.Equal(t, "mem_1", tasks.executionOwnerMemberID)
	require.Equal(t, 1, access.calls)
	require.Equal(t, 1, messages.calls)
	require.Equal(t, 1, members.calls)
	require.Equal(t, 1, tasks.listCalls)
	require.Equal(t, 1, tasks.capabilityCalls)
	require.Less(t, access.order, messages.order)
	require.Less(t, access.order, members.order)
	require.Less(t, access.order, tasks.listOrder)
	require.Less(t, access.order, tasks.capabilityOrder)
}

func TestScopeSnapshotAccessNonJoinedShortCircuitsPrivateStores(t *testing.T) {
	tests := []struct {
		name       string
		resolution channel.ChannelAccessResolution
		wantErr    error
		wantStatus int
	}{
		{
			name: "public joinable",
			resolution: channel.ChannelAccessResolution{
				Status: channel.ChannelAccessStatusPublicJoinable,
				JoinableChannel: channel.DiscoverableChannel{
					ChannelID:  "chn_1",
					Visibility: "public",
				},
			},
			wantErr:    api.ErrForbidden,
			wantStatus: 403,
		},
		{
			name:       "inaccessible",
			resolution: channel.ChannelAccessResolution{Status: channel.ChannelAccessStatusInaccessible},
			wantErr:    api.ErrForbidden,
			wantStatus: 403,
		},
		{
			name:       "not found",
			resolution: channel.ChannelAccessResolution{Status: channel.ChannelAccessStatusNotFound},
			wantErr:    api.ErrNotFound,
			wantStatus: 404,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			access := &fakeScopeSnapshotAccessResolver{resolution: tt.resolution}
			messages := &fakeScopeSnapshotMessageStore{}
			members := &fakeScopeSnapshotChannelMemberStore{}
			tasks := &fakeScopeSnapshotTaskBoard{}
			handler := &ScopeSnapshotHandler{access: access, messages: messages, channelMembers: members, taskBoard: tasks}

			request := imWorkspaceRequest(gin.Param{Key: "scopeId", Value: "chn_1"})
			request.Gin = ginContextWithQuery(request.Gin, "include=messages,tasks,members,privateAgentCapability&executionOwnerMemberId=mem_1")
			response, err := handler.Get(request)

			require.ErrorIs(t, err, tt.wantErr)
			require.Equal(t, tt.wantStatus, api.StatusForError(err))
			require.Equal(t, api.Response{}, response)
			require.Equal(t, 1, access.calls)
			require.Zero(t, messages.calls)
			require.Zero(t, members.calls)
			require.Zero(t, tasks.listCalls)
			require.Zero(t, tasks.capabilityCalls)
		})
	}
}

func TestScopeSnapshotAccessJoinedPrivateReadFailureReturnsNoPartialBody(t *testing.T) {
	messageErr := errors.New("messages unavailable")
	access := &fakeScopeSnapshotAccessResolver{
		resolution: channel.ChannelAccessResolution{
			Status:  channel.ChannelAccessStatusJoined,
			Channel: message.ChannelSummary{ChannelID: "chn_1", Joined: true},
		},
	}
	messages := &fakeScopeSnapshotMessageStore{err: messageErr}
	members := &fakeScopeSnapshotChannelMemberStore{}
	tasks := &fakeScopeSnapshotTaskBoard{}
	handler := &ScopeSnapshotHandler{access: access, messages: messages, channelMembers: members, taskBoard: tasks}
	request := imWorkspaceRequest(gin.Param{Key: "scopeId", Value: "chn_1"})
	request.Gin = ginContextWithQuery(request.Gin, "include=messages,members")

	response, err := handler.Get(request)
	require.ErrorIs(t, err, messageErr)
	require.Equal(t, api.Response{}, response)
	require.Equal(t, 1, access.calls)
	require.Equal(t, 1, messages.calls)
	require.Zero(t, members.calls)
	require.Zero(t, tasks.listCalls)
	require.Zero(t, tasks.capabilityCalls)
}

func TestScopeSnapshotAccessPreservesGenericNonChannelMessageRead(t *testing.T) {
	access := &fakeScopeSnapshotAccessResolver{
		resolution: channel.ChannelAccessResolution{
			Status: channel.ChannelAccessStatusNotFound,
		},
	}
	messages := &fakeScopeSnapshotMessageStore{page: message.AppMessagePage{
		Messages: []message.AppMessage{{ID: "msg_thread", ChannelID: "thr_1"}},
	}}
	handler := &ScopeSnapshotHandler{
		access:         access,
		messages:       messages,
		channelMembers: &fakeScopeSnapshotChannelMemberStore{},
		taskBoard:      &fakeScopeSnapshotTaskBoard{},
	}
	request := imWorkspaceRequest(gin.Param{Key: "scopeId", Value: "thr_1"})
	request.Gin = ginContextWithQuery(request.Gin, "include=messages")

	response, err := handler.Get(request)
	require.NoError(t, err)
	body, ok := response.Body.(scopeSnapshotResponse)
	require.True(t, ok)
	require.Equal(t, "msg_thread", body.Messages.Messages[0].ID)
	require.Zero(t, access.calls, "generic message-only snapshots must preserve their existing store authorization path")
	require.Equal(t, 1, messages.calls)
}

func TestScopeSnapshotAccessErrorShortCircuitsPrivateStores(t *testing.T) {
	accessErr := errors.New("access unavailable")
	access := &fakeScopeSnapshotAccessResolver{err: accessErr}
	messages := &fakeScopeSnapshotMessageStore{}
	members := &fakeScopeSnapshotChannelMemberStore{}
	tasks := &fakeScopeSnapshotTaskBoard{}
	handler := &ScopeSnapshotHandler{access: access, messages: messages, channelMembers: members, taskBoard: tasks}

	request := imWorkspaceRequest(gin.Param{Key: "scopeId", Value: "chn_1"})
	request.Gin = ginContextWithQuery(request.Gin, "include=messages,members")
	response, err := handler.Get(request)
	require.ErrorIs(t, err, accessErr)
	require.Equal(t, api.Response{}, response)
	require.Equal(t, 1, access.calls)
	require.Zero(t, messages.calls)
	require.Zero(t, members.calls)
	require.Zero(t, tasks.listCalls)
	require.Zero(t, tasks.capabilityCalls)
}

type fakeScopeSnapshotAccessResolver struct {
	resolution channel.ChannelAccessResolution
	err        error
	calls      int
	order      int
}

func (resolver *fakeScopeSnapshotAccessResolver) ResolveChannelAccessForUser(context.Context, string, string, string) (channel.ChannelAccessResolution, error) {
	resolver.calls++
	resolver.order = nextScopeSnapshotCallOrder()
	return resolver.resolution, resolver.err
}

type fakeScopeSnapshotMessageStore struct {
	page        message.AppMessagePage
	userID      string
	workspaceID string
	scopeID     string
	limit       int
	calls       int
	order       int
	err         error
}

func (store *fakeScopeSnapshotMessageStore) ListMessagesAroundForUser(context.Context, string, string, string, string, int) (message.AppMessagePage, error) {
	return message.AppMessagePage{}, nil
}

func (store *fakeScopeSnapshotMessageStore) ListMessagesForUser(_ context.Context, userID string, workspaceID string, scopeID string, limit int, _ *int64, _ *int64) (message.AppMessagePage, error) {
	store.calls++
	store.order = nextScopeSnapshotCallOrder()
	store.userID = userID
	store.workspaceID = workspaceID
	store.scopeID = scopeID
	store.limit = limit
	return store.page, store.err
}

type fakeScopeSnapshotChannelMemberStore struct {
	members message.ChannelMemberList
	calls   int
	order   int
}

func (store *fakeScopeSnapshotChannelMemberStore) ChannelMembersForUser(context.Context, string, string, string) (message.ChannelMemberList, error) {
	store.calls++
	store.order = nextScopeSnapshotCallOrder()
	return store.members, nil
}

type fakeScopeSnapshotTaskBoard struct {
	list                   taskboard.ListTasksResponse
	capability             taskboard.ResolvePrivateAgentContextContributionCapabilityResponse
	includeSubtasks        bool
	executionOwnerMemberID string
	listCalls              int
	listOrder              int
	capabilityCalls        int
	capabilityOrder        int
}

func (board *fakeScopeSnapshotTaskBoard) ListTasks(_ context.Context, request taskboard.ListTasksRequest) (taskboard.ListTasksResponse, error) {
	board.listCalls++
	board.listOrder = nextScopeSnapshotCallOrder()
	board.includeSubtasks = request.IncludeSubtasks
	return board.list, nil
}

func (board *fakeScopeSnapshotTaskBoard) ResolvePrivateAgentContextContributionCapability(_ context.Context, request taskboard.ResolvePrivateAgentContextContributionCapabilityRequest) (taskboard.ResolvePrivateAgentContextContributionCapabilityResponse, error) {
	board.capabilityCalls++
	board.capabilityOrder = nextScopeSnapshotCallOrder()
	board.executionOwnerMemberID = request.Scope.ExecutionOwnerMemberID
	return board.capability, nil
}

var scopeSnapshotCallOrder int

func nextScopeSnapshotCallOrder() int {
	scopeSnapshotCallOrder++
	return scopeSnapshotCallOrder
}

func ginContextWithQuery(ctx *gin.Context, rawQuery string) *gin.Context {
	ctx.Request = httptest.NewRequest("GET", "/?"+rawQuery, nil)
	return ctx
}
