package auth

import (
	"bytes"
	"context"
	"net/http"
	"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/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/core/authidentity"
	"github.com/tidbcloud/AgentTeam/internal/service/authflow"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
	"github.com/tidbcloud/AgentTeam/internal/store/organization"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
)

type fakeInviteAuthFlow struct {
	pendingInvites     []authflow.PendingInvite
	listCalls          int
	info               authflow.InviteInfo
	infoFound          bool
	accepted           authflow.InviteAcceptance
	acceptedFound      bool
	acceptedByToken    authflow.InviteAcceptance
	tokenAcceptedFound bool
	acceptPendingCalls int
	acceptTokenCalls   int
	dismissedFound     bool
	dismissedInviteID  string
	dismissCalls       int
}

func (flow *fakeInviteAuthFlow) RegistrationAllowedForEmail(context.Context, string, bool) (bool, error) {
	return false, nil
}

func (flow *fakeInviteAuthFlow) EnsureDefaultWorkspace(context.Context, identity.User, authflow.DefaultWorkspaceOptions) error {
	return nil
}

func (flow *fakeInviteAuthFlow) FindInviteInfoByToken(context.Context, string) (authflow.InviteInfo, bool, error) {
	return authflow.InviteInfo{}, false, nil
}

func (flow *fakeInviteAuthFlow) FindWorkspaceInviteInfoByID(context.Context, string) (authflow.InviteInfo, bool, error) {
	return flow.info, flow.infoFound, nil
}

func (flow *fakeInviteAuthFlow) AcceptInviteByToken(context.Context, identity.User, string) (authflow.InviteAcceptance, bool, error) {
	flow.acceptTokenCalls++
	return flow.acceptedByToken, flow.tokenAcceptedFound, nil
}

func (flow *fakeInviteAuthFlow) ListPendingInvitesForUser(context.Context, identity.User) ([]authflow.PendingInvite, error) {
	flow.listCalls++
	return flow.pendingInvites, nil
}

func (flow *fakeInviteAuthFlow) AcceptPendingInviteByID(context.Context, identity.User, string) (authflow.InviteAcceptance, bool, error) {
	flow.acceptPendingCalls++
	return flow.accepted, flow.acceptedFound, nil
}

func (flow *fakeInviteAuthFlow) DismissPendingInviteByID(_ context.Context, _ identity.User, inviteID string) (bool, error) {
	flow.dismissCalls++
	flow.dismissedInviteID = inviteID
	return flow.dismissedFound, nil
}

func (flow *fakeInviteAuthFlow) AutoJoinOrganizationsAfterVerifiedLogin(context.Context, identity.User, authidentity.Kind) error {
	return nil
}

type fakeInviteRealtime struct {
	channelUpdates   []string
	joinedChannels   []string
	workspaceUpdates []string
}

func (realtime *fakeInviteRealtime) EmitChannelMembersUpdated(workspaceID string, channelID string) {
	realtime.channelUpdates = append(realtime.channelUpdates, workspaceID+":"+channelID)
}

func (realtime *fakeInviteRealtime) SubscribeScopeForUser(workspaceID string, userID string, channelID string) {
	realtime.joinedChannels = append(realtime.joinedChannels, workspaceID+":"+userID+":"+channelID)
}

func (realtime *fakeInviteRealtime) EmitWorkspaceMembersUpdated(workspaceID string) {
	realtime.workspaceUpdates = append(realtime.workspaceUpdates, workspaceID)
}

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

	handler := &InviteHandler{}
	router := gin.New()
	router.GET("/api/v1/auth/invite-info", handler.Info)

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/auth/invite-info", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusBadRequest, recorder.Code)
	require.Contains(t, recorder.Body.String(), "token is required")
}

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

	now := time.Date(2026, 5, 8, 12, 0, 0, 0, time.UTC)
	handler := &InviteHandler{
		authFlow: &fakeInviteAuthFlow{pendingInvites: []authflow.PendingInvite{{
			Kind: authflow.InviteKindWorkspace,
			Workspace: workspace.PendingWorkspaceInvite{
				ID:                "inv_1",
				WorkspaceID:       "wsp_1",
				WorkspaceName:     "Team",
				InviterName:       "Ada",
				InvitedMobileHint: "+8613*******00",
				CreatedAt:         now,
				ExpiresAt:         now.Add(24 * time.Hour),
			},
		}}},
	}
	router := gin.New()
	router.GET("/api/v1/auth/pending-invites", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1", Mobile: "+8613800138000", MobileVerifiedAt: &now}})
	}, handleAuth(handler.Pending))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/auth/pending-invites", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.JSONEq(t, `{"invites":[{"kind":"workspace","inviteId":"inv_1","workspaceId":"wsp_1","workspaceName":"Team","inviterName":"Ada","invitedMobileHint":"+8613*******00","createdAt":"2026-05-08T12:00:00Z","expiresAt":"2026-05-09T12:00:00Z"}]}`, recorder.Body.String())
}

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

	flow := &fakeInviteAuthFlow{pendingInvites: []authflow.PendingInvite{{
		Kind: authflow.InviteKindWorkspace,
		Workspace: workspace.PendingWorkspaceInvite{
			ID:                "inv_1",
			InvitedMobileHint: "+8613*******00",
		},
	}}}
	handler := &InviteHandler{authFlow: flow}
	router := gin.New()
	router.GET("/api/v1/auth/pending-invites", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{
			ID:    "usr_1",
			Email: "invitee@example.com",
		}})
	}, handleAuth(handler.Pending))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/auth/pending-invites", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.JSONEq(t, `{"invites":[]}`, recorder.Body.String())
	require.Equal(t, 0, flow.listCalls)
}

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

	flow := &fakeInviteAuthFlow{
		accepted: authflow.InviteAcceptance{
			Kind:      authflow.InviteKindWorkspace,
			Workspace: workspace.Summary{WorkspaceID: "wsp_1", Name: "Team"},
		},
		acceptedFound: true,
	}
	handler := &InviteHandler{authFlow: flow}
	router := gin.New()
	router.POST("/api/v1/auth/pending-invites/:inviteId/accept", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{
			ID:    "usr_1",
			Email: "invitee@example.com",
		}})
	}, handleAuth(handler.AcceptPending))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/pending-invites/inv_1/accept", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusForbidden, recorder.Code)
	require.JSONEq(t, `{"error":"Email or mobile must be verified to accept this invite"}`, recorder.Body.String())
	require.Equal(t, 0, flow.acceptPendingCalls)
}

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

	flow := &fakeInviteAuthFlow{dismissedFound: true}
	handler := &InviteHandler{authFlow: flow}
	router := gin.New()
	router.POST("/api/v1/auth/pending-invites/:inviteId/dismiss", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{
			ID:    "usr_1",
			Email: "invitee@example.com",
		}})
	}, handleAuth(handler.DismissPending))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/pending-invites/inv_1/dismiss", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusForbidden, recorder.Code)
	require.JSONEq(t, `{"error":"Email or mobile must be verified to dismiss this invite"}`, recorder.Body.String())
	require.Equal(t, 0, flow.dismissCalls)
}

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

	now := time.Date(2026, 5, 8, 12, 0, 0, 0, time.UTC)
	flow := &fakeInviteAuthFlow{dismissedFound: true}
	handler := &InviteHandler{authFlow: flow}
	router := gin.New()
	router.POST("/api/v1/auth/pending-invites/:inviteId/dismiss", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1", Mobile: "+8613800138000", MobileVerifiedAt: &now}})
	}, handleAuth(handler.DismissPending))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/pending-invites/inv_1/dismiss", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.JSONEq(t, `{}`, recorder.Body.String())
	require.Equal(t, 1, flow.dismissCalls)
	require.Equal(t, "inv_1", flow.dismissedInviteID)
}

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

	now := time.Date(2026, 5, 8, 12, 0, 0, 0, time.UTC)
	realtime := &fakeInviteRealtime{}
	handler := &InviteHandler{
		authFlow: &fakeInviteAuthFlow{
			accepted: authflow.InviteAcceptance{
				Kind:      authflow.InviteKindWorkspace,
				Workspace: workspace.Summary{WorkspaceID: "wsp_1", Name: "Team", MemberUserID: "usr_1", AllChannelID: "chn_all"},
			},
			acceptedFound: true,
		},
		realtime: realtime,
	}
	router := gin.New()
	router.POST("/api/v1/auth/pending-invites/:inviteId/accept", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1", Mobile: "+8613800138000", MobileVerifiedAt: &now}})
	}, handleAuth(handler.AcceptPending))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/pending-invites/inv_1/accept", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.JSONEq(t, `{"kind":"workspace","workspaceId":"wsp_1","workspaceName":"Team"}`, recorder.Body.String())
	require.Equal(t, []string{"wsp_1"}, realtime.workspaceUpdates)
	require.Equal(t, []string{"wsp_1:chn_all"}, realtime.channelUpdates)
	require.Equal(t, []string{"wsp_1:usr_1:chn_all"}, realtime.joinedChannels)
}

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

	handler := &InviteHandler{
		authFlow: &fakeInviteAuthFlow{
			info: authflow.InviteInfo{
				Kind: authflow.InviteKindWorkspace,
				Workspace: workspace.WorkspaceInviteInfo{
					WorkspaceID:       "wsp_1",
					WorkspaceName:     "Team",
					InviterName:       "Ada",
					InvitedMobileHint: "+8613*******00",
				},
			},
			infoFound: true,
		},
	}
	router := gin.New()
	router.GET("/api/v1/auth/pending-invites/:inviteId", handler.PendingInfo)

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/auth/pending-invites/inv_1", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.JSONEq(t, `{"kind":"workspace","workspaceId":"wsp_1","workspaceName":"Team","inviterName":"Ada","invitedMobileHint":"+8613*******00"}`, recorder.Body.String())
}

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

	handler := &InviteHandler{}
	router := gin.New()
	router.POST("/api/v1/auth/accept-invite", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1", Email: "user@example.com"}})
	}, handleAuthJSON[dto.AcceptInviteRequest](handler.Accept))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/accept-invite", bytes.NewBufferString(`{"token":"   "}`))
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusBadRequest, recorder.Code)
	require.Contains(t, recorder.Body.String(), "token is required")
}

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

	realtime := &fakeInviteRealtime{}
	flow := &fakeInviteAuthFlow{
		acceptedByToken: authflow.InviteAcceptance{
			Kind:      authflow.InviteKindWorkspace,
			Workspace: workspace.Summary{WorkspaceID: "wsp_1", Name: "Team", MemberUserID: "usr_1", AllChannelID: "chn_all"},
		},
		tokenAcceptedFound: true,
	}
	handler := &InviteHandler{
		authFlow: flow,
		realtime: realtime,
	}
	router := gin.New()
	router.POST("/api/v1/auth/accept-invite", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1", Email: "user@example.com"}})
	}, handleAuthJSON[dto.AcceptInviteRequest](handler.Accept))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/accept-invite", bytes.NewBufferString(`{"token":"tok_1"}`))
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.JSONEq(t, `{"kind":"workspace","workspaceId":"wsp_1","workspaceName":"Team"}`, recorder.Body.String())
	require.Equal(t, 1, flow.acceptTokenCalls)
	require.Equal(t, []string{"wsp_1"}, realtime.workspaceUpdates)
	require.Equal(t, []string{"wsp_1:chn_all"}, realtime.channelUpdates)
	require.Equal(t, []string{"wsp_1:usr_1:chn_all"}, realtime.joinedChannels)
}

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

	realtime := &fakeInviteRealtime{}
	flow := &fakeInviteAuthFlow{
		acceptedByToken: authflow.InviteAcceptance{
			Kind: authflow.InviteKindOrganization,
			Organization: organization.OrganizationInviteAcceptance{
				OrganizationID:   "org_1",
				OrganizationName: "Org",
				Role:             "member",
			},
		},
		tokenAcceptedFound: true,
	}
	handler := &InviteHandler{
		authFlow: flow,
		realtime: realtime,
	}
	router := gin.New()
	router.POST("/api/v1/auth/accept-invite", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1", Email: "user@example.com"}})
	}, handleAuthJSON[dto.AcceptInviteRequest](handler.Accept))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/accept-invite", bytes.NewBufferString(`{"token":"tok_1"}`))
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.JSONEq(t, `{"kind":"organization","organizationId":"org_1","organizationName":"Org","role":"member"}`, recorder.Body.String())
	require.Empty(t, realtime.workspaceUpdates)
	require.Empty(t, realtime.channelUpdates)
	require.Empty(t, realtime.joinedChannels)
}
