package authflow

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/core/authidentity"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
	"github.com/tidbcloud/AgentTeam/internal/store/organization"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
)

type fakeRegistrationAllowlist struct {
	patterns []string
	err      error
}

func (store fakeRegistrationAllowlist) ListActiveRegistrationAllowlistPatterns(context.Context) ([]string, error) {
	return append([]string(nil), store.patterns...), store.err
}

type fakeWorkspaceInvites struct {
	hasPending         bool
	workspaceInfo      workspace.WorkspaceInviteInfo
	workspaceInfoFound bool
	pendingInvites     []workspace.PendingWorkspaceInvite
	listCalls          int
	dismissedFound     bool
	dismissedInviteID  string
	dismissCalls       int
	accepted           workspace.Summary
	acceptedFound      bool
	acceptedByID       workspace.Summary
	acceptedByIDFound  bool
	acceptByIDCalls    int
}

func (store fakeWorkspaceInvites) HasPendingWorkspaceInviteForEmail(context.Context, string) (bool, error) {
	return store.hasPending, nil
}

func (store fakeWorkspaceInvites) FindWorkspaceInviteInfoByToken(context.Context, string) (workspace.WorkspaceInviteInfo, bool, error) {
	return store.workspaceInfo, store.workspaceInfoFound, nil
}

func (store fakeWorkspaceInvites) FindWorkspaceInviteInfoByID(context.Context, string) (workspace.WorkspaceInviteInfo, bool, error) {
	return store.workspaceInfo, store.workspaceInfoFound, nil
}

func (store fakeWorkspaceInvites) AcceptWorkspaceInviteByTokenIfExists(context.Context, identity.User, string) (workspace.Summary, bool, error) {
	return store.accepted, store.acceptedFound, nil
}

func (store *fakeWorkspaceInvites) ListPendingWorkspaceInvitesForUser(context.Context, identity.User) ([]workspace.PendingWorkspaceInvite, error) {
	store.listCalls++
	return store.pendingInvites, nil
}

func (store *fakeWorkspaceInvites) AcceptWorkspaceInviteByIDIfExists(context.Context, identity.User, string) (workspace.Summary, bool, error) {
	store.acceptByIDCalls++
	return store.acceptedByID, store.acceptedByIDFound, nil
}

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

type fakeOrganizations struct {
	hasPending          bool
	orgInfo             organization.OrganizationInviteInfo
	orgInfoFound        bool
	accepted            organization.OrganizationInviteAcceptance
	acceptedFound       bool
	defaultWorkspace    bool
	emailAutoJoinCalls  int
	mobileAutoJoinCalls int
	emailAutoJoinErr    error
}

func (orgs *fakeOrganizations) HasPendingOrganizationInviteForEmail(context.Context, string) (bool, error) {
	return orgs.hasPending, nil
}

func (orgs *fakeOrganizations) FindOrganizationInviteInfoByToken(context.Context, string) (organization.OrganizationInviteInfo, bool, error) {
	return orgs.orgInfo, orgs.orgInfoFound, nil
}

func (orgs *fakeOrganizations) AcceptOrganizationInviteByTokenIfExists(context.Context, identity.User, string) (organization.OrganizationInviteAcceptance, bool, error) {
	return orgs.accepted, orgs.acceptedFound, nil
}

func (orgs *fakeOrganizations) AutoJoinOrganizationsForVerifiedEmail(context.Context, identity.User) error {
	orgs.emailAutoJoinCalls++
	return orgs.emailAutoJoinErr
}

func (orgs *fakeOrganizations) AutoJoinOrganizationsForVerifiedMobile(context.Context, identity.User) error {
	orgs.mobileAutoJoinCalls++
	return nil
}

func (orgs *fakeOrganizations) EnsureDefaultWorkspaceForUser(context.Context, identity.User) error {
	orgs.defaultWorkspace = true
	return nil
}

type fakeReportingOrganizations struct {
	fakeOrganizations
	organizationID string
	shouldRun      bool
	ensureErr      error
	markErr        error
	ensureCalls    int
	markCalls      int
	markedUserID   string
	markedOrgID    string
}

func (orgs *fakeReportingOrganizations) EnsureDefaultWorkspaceForUserAndReport(context.Context, identity.User) (string, bool, error) {
	orgs.ensureCalls++
	return orgs.organizationID, orgs.shouldRun, orgs.ensureErr
}

func (orgs *fakeReportingOrganizations) MarkDefaultWorkspaceBootstrapCompleted(_ context.Context, userID string, organizationID string) error {
	orgs.markCalls++
	orgs.markedUserID = userID
	orgs.markedOrgID = organizationID
	return orgs.markErr
}

type fakeDefaultWorkspacePostCommit struct {
	calls          int
	organizationID string
	userID         string
	err            error
}

func (provisioner *fakeDefaultWorkspacePostCommit) EnsureDefaultWorkspacePostCommit(_ context.Context, organizationID string, userID string) error {
	provisioner.calls++
	provisioner.organizationID = organizationID
	provisioner.userID = userID
	return provisioner.err
}

func TestRegistrationAllowedForEmailChecksStaticAllowlistFirst(t *testing.T) {
	allowed, err := New(Dependencies{}).RegistrationAllowedForEmail(context.Background(), "alice@example.com", true)
	require.NoError(t, err)
	require.True(t, allowed)
}

func TestRegistrationAllowedForEmailChecksDatabaseAndInvites(t *testing.T) {
	service := New(Dependencies{
		RegistrationAllowlist: fakeRegistrationAllowlist{patterns: []string{"@example.com"}},
	})
	allowed, err := service.RegistrationAllowedForEmail(context.Background(), "alice@example.com", false)
	require.NoError(t, err)
	require.True(t, allowed)

	service = New(Dependencies{WorkspaceInvites: &fakeWorkspaceInvites{hasPending: true}})
	allowed, err = service.RegistrationAllowedForEmail(context.Background(), "bob@example.com", false)
	require.NoError(t, err)
	require.True(t, allowed)

	service = New(Dependencies{Organizations: &fakeOrganizations{hasPending: true}})
	allowed, err = service.RegistrationAllowedForEmail(context.Background(), "carol@example.com", false)
	require.NoError(t, err)
	require.True(t, allowed)
}

func TestInviteInfoAndAcceptancePreferWorkspaceThenOrganization(t *testing.T) {
	service := New(Dependencies{
		WorkspaceInvites: &fakeWorkspaceInvites{
			workspaceInfo:      workspace.WorkspaceInviteInfo{WorkspaceID: "srv_1", WorkspaceName: "AgentTeam"},
			workspaceInfoFound: true,
			accepted:           workspace.Summary{WorkspaceID: "srv_1", Name: "AgentTeam"},
			acceptedFound:      true,
		},
		Organizations: &fakeOrganizations{
			orgInfo:       organization.OrganizationInviteInfo{OrganizationID: "org_1"},
			orgInfoFound:  true,
			accepted:      organization.OrganizationInviteAcceptance{OrganizationID: "org_1"},
			acceptedFound: true,
		},
	})

	info, found, err := service.FindInviteInfoByToken(context.Background(), "tok")
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, InviteKindWorkspace, info.Kind)
	require.Equal(t, "srv_1", info.Workspace.WorkspaceID)

	accepted, found, err := service.AcceptInviteByToken(context.Background(), identity.User{ID: "usr_1"}, "tok")
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, InviteKindWorkspace, accepted.Kind)
	require.Equal(t, "srv_1", accepted.Workspace.WorkspaceID)
}

func TestInviteInfoFallsBackToOrganization(t *testing.T) {
	service := New(Dependencies{
		WorkspaceInvites: &fakeWorkspaceInvites{},
		Organizations: &fakeOrganizations{
			orgInfo:       organization.OrganizationInviteInfo{OrganizationID: "org_1"},
			orgInfoFound:  true,
			accepted:      organization.OrganizationInviteAcceptance{OrganizationID: "org_1"},
			acceptedFound: true,
		},
	})

	info, found, err := service.FindInviteInfoByToken(context.Background(), "tok")
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, InviteKindOrganization, info.Kind)
	require.Equal(t, "org_1", info.Organization.OrganizationID)

	accepted, found, err := service.AcceptInviteByToken(context.Background(), identity.User{ID: "usr_1"}, "tok")
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, InviteKindOrganization, accepted.Kind)
	require.Equal(t, "org_1", accepted.Organization.OrganizationID)
}

func TestPendingInvitesAndAcceptanceByIDUseWorkspaceInvites(t *testing.T) {
	now := time.Now().UTC()
	service := New(Dependencies{
		WorkspaceInvites: &fakeWorkspaceInvites{
			pendingInvites: []workspace.PendingWorkspaceInvite{{
				ID:            "inv_1",
				WorkspaceID:   "wsp_1",
				WorkspaceName: "AgentTeam",
			}},
			acceptedByID:      workspace.Summary{WorkspaceID: "wsp_1", Name: "AgentTeam"},
			acceptedByIDFound: true,
		},
	})

	pending, err := service.ListPendingInvitesForUser(context.Background(), identity.User{ID: "usr_1", Mobile: "+8613800138000", MobileVerifiedAt: &now})
	require.NoError(t, err)
	require.Len(t, pending, 1)
	require.Equal(t, InviteKindWorkspace, pending[0].Kind)
	require.Equal(t, "inv_1", pending[0].Workspace.ID)

	accepted, found, err := service.AcceptPendingInviteByID(context.Background(), identity.User{ID: "usr_1", Mobile: "+8613800138000", MobileVerifiedAt: &now}, "inv_1")
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, InviteKindWorkspace, accepted.Kind)
	require.Equal(t, "wsp_1", accepted.Workspace.WorkspaceID)
}

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

	now := time.Now().UTC()
	invites := &fakeWorkspaceInvites{dismissedFound: true}
	service := New(Dependencies{WorkspaceInvites: invites})

	found, err := service.DismissPendingInviteByID(context.Background(), identity.User{ID: "usr_1", Mobile: "+8613800138000", MobileVerifiedAt: &now}, "inv_1")
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, 1, invites.dismissCalls)
	require.Equal(t, "inv_1", invites.dismissedInviteID)
}

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

	invites := &fakeWorkspaceInvites{
		pendingInvites:    []workspace.PendingWorkspaceInvite{{ID: "inv_1"}},
		acceptedByID:      workspace.Summary{WorkspaceID: "wsp_1", Name: "AgentTeam"},
		acceptedByIDFound: true,
	}
	service := New(Dependencies{WorkspaceInvites: invites})
	unverifiedUser := identity.User{
		ID:    "usr_1",
		Email: "invitee@example.com",
	}

	pending, err := service.ListPendingInvitesForUser(context.Background(), unverifiedUser)
	require.NoError(t, err)
	require.Empty(t, pending)
	require.Equal(t, 0, invites.listCalls)

	accepted, found, err := service.AcceptPendingInviteByID(context.Background(), unverifiedUser, "inv_1")
	require.ErrorIs(t, err, storeerr.ErrForbidden)
	require.False(t, found)
	require.Empty(t, accepted)
	require.Equal(t, 0, invites.acceptByIDCalls)

	dismissed, err := service.DismissPendingInviteByID(context.Background(), unverifiedUser, "inv_1")
	require.ErrorIs(t, err, storeerr.ErrForbidden)
	require.False(t, dismissed)
	require.Equal(t, 0, invites.dismissCalls)
}

func TestEnsureDefaultWorkspaceDelegatesToOrganizationPort(t *testing.T) {
	orgs := &fakeOrganizations{}
	err := New(Dependencies{Organizations: orgs}).EnsureDefaultWorkspace(context.Background(), identity.User{ID: "usr_1"}, DefaultWorkspaceOptions{})
	require.NoError(t, err)
	require.True(t, orgs.defaultWorkspace)
}

func TestEnsureDefaultWorkspaceRunsPostCommitBeforeMarkingCompleted(t *testing.T) {
	orgs := &fakeReportingOrganizations{
		organizationID: "org_1",
		shouldRun:      true,
	}
	postCommit := &fakeDefaultWorkspacePostCommit{}

	err := New(Dependencies{
		Organizations:                     orgs,
		DefaultWorkspacePostCommit:        postCommit,
		RequireDefaultWorkspacePostCommit: true,
	}).EnsureDefaultWorkspace(context.Background(), identity.User{ID: "usr_1"}, DefaultWorkspaceOptions{ProvisionOnboardingTrial: true})

	require.NoError(t, err)
	require.Equal(t, 1, orgs.ensureCalls)
	require.Equal(t, 1, postCommit.calls)
	require.Equal(t, "org_1", postCommit.organizationID)
	require.Equal(t, "usr_1", postCommit.userID)
	require.Equal(t, 1, orgs.markCalls)
	require.Equal(t, "usr_1", orgs.markedUserID)
	require.Equal(t, "org_1", orgs.markedOrgID)
}

func TestEnsureDefaultWorkspaceDoesNotMarkCompletedWhenPostCommitFails(t *testing.T) {
	expected := errors.New("maas unavailable")
	orgs := &fakeReportingOrganizations{
		organizationID: "org_1",
		shouldRun:      true,
	}
	postCommit := &fakeDefaultWorkspacePostCommit{err: expected}

	err := New(Dependencies{
		Organizations:                     orgs,
		DefaultWorkspacePostCommit:        postCommit,
		RequireDefaultWorkspacePostCommit: true,
	}).EnsureDefaultWorkspace(context.Background(), identity.User{ID: "usr_1"}, DefaultWorkspaceOptions{ProvisionOnboardingTrial: true})

	require.ErrorIs(t, err, expected)
	require.Equal(t, 1, postCommit.calls)
	require.Equal(t, 0, orgs.markCalls)
}

func TestEnsureDefaultWorkspaceSkipsPostCommitForCompletedBootstrap(t *testing.T) {
	orgs := &fakeReportingOrganizations{
		organizationID: "org_1",
		shouldRun:      false,
	}
	postCommit := &fakeDefaultWorkspacePostCommit{}

	err := New(Dependencies{
		Organizations:                     orgs,
		DefaultWorkspacePostCommit:        postCommit,
		RequireDefaultWorkspacePostCommit: true,
	}).EnsureDefaultWorkspace(context.Background(), identity.User{ID: "usr_1"}, DefaultWorkspaceOptions{ProvisionOnboardingTrial: true})

	require.NoError(t, err)
	require.Equal(t, 1, orgs.ensureCalls)
	require.Equal(t, 0, postCommit.calls)
	require.Equal(t, 0, orgs.markCalls)
}

func TestEnsureDefaultWorkspaceCompletesBootstrapWithoutUnrequestedOnboardingTrial(t *testing.T) {
	orgs := &fakeReportingOrganizations{
		organizationID: "org_1",
		shouldRun:      true,
	}
	postCommit := &fakeDefaultWorkspacePostCommit{}

	err := New(Dependencies{
		Organizations:                     orgs,
		DefaultWorkspacePostCommit:        postCommit,
		RequireDefaultWorkspacePostCommit: true,
	}).EnsureDefaultWorkspace(context.Background(), identity.User{ID: "usr_1"}, DefaultWorkspaceOptions{})

	require.NoError(t, err)
	require.Equal(t, 1, orgs.ensureCalls)
	require.Equal(t, 0, postCommit.calls)
	require.Equal(t, 1, orgs.markCalls)
	require.Equal(t, "usr_1", orgs.markedUserID)
	require.Equal(t, "org_1", orgs.markedOrgID)
}

func TestAutoJoinOrganizationsAfterVerifiedLoginUsesIdentifierPrecedence(t *testing.T) {
	now := time.Now().UTC()
	orgs := &fakeOrganizations{}
	user := identity.User{ID: "usr_1", Email: "alice@example.com", Mobile: "+8613000000000", EmailVerifiedAt: &now, MobileVerifiedAt: &now}

	err := New(Dependencies{Organizations: orgs}).AutoJoinOrganizationsAfterVerifiedLogin(context.Background(), user, authidentity.KindMobile)
	require.NoError(t, err)
	require.Equal(t, 0, orgs.emailAutoJoinCalls)
	require.Equal(t, 1, orgs.mobileAutoJoinCalls)
}

func TestAutoJoinOrganizationsAfterVerifiedLoginReturnsPreferredErrorWithoutFallback(t *testing.T) {
	now := time.Now().UTC()
	expected := errors.New("auto join failed")
	orgs := &fakeOrganizations{emailAutoJoinErr: expected}
	user := identity.User{ID: "usr_1", Email: "alice@example.com", Mobile: "+8613000000000", EmailVerifiedAt: &now, MobileVerifiedAt: &now}

	err := New(Dependencies{Organizations: orgs}).AutoJoinOrganizationsAfterVerifiedLogin(context.Background(), user, authidentity.KindEmail)
	require.ErrorIs(t, err, expected)
	require.Equal(t, 1, orgs.emailAutoJoinCalls)
	require.Equal(t, 0, orgs.mobileAutoJoinCalls)
}
