package organization

import (
	"context"
	"io"
	"log/slog"
	"slices"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/archive"
	"github.com/tidbcloud/AgentTeam/internal/core/secrets"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
)

type serviceTestRuntime struct {
	ctx     context.Context
	pool    coredb.DB
	store   *store.Graph
	service *Service
}

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

	runtime := startOrganizationServiceRuntime(t)
	owner := bootstrapOrganizationAdmin(t, runtime, "owner@example.com", "owner")
	target := createVerifiedOrganizationUser(t, runtime, "target@example.com", "target")

	organization, err := runtime.service.CreateOrganizationForUser(runtime.ctx, owner, "Acme Org", "acme-org")
	require.NoError(t, err)

	units, err := runtime.service.ListOrganizationUnitsForUser(runtime.ctx, owner.ID, organization.ID)
	require.NoError(t, err)
	require.Len(t, units, 1)
	require.Equal(t, "General", units[0].Name)
	require.Equal(t, "general", units[0].Slug)
	require.True(t, units[0].IsDefault)
	require.Equal(t, 1, units[0].MemberCount)

	account, err := runtime.service.CreateOrganizationMemberForUser(runtime.ctx, owner.ID, organization.ID, "", target.Email, "member")
	require.NoError(t, err)

	units, err = runtime.service.ListOrganizationUnitsForUser(runtime.ctx, owner.ID, organization.ID)
	require.NoError(t, err)
	require.Len(t, units, 1)
	require.Equal(t, 2, units[0].MemberCount)

	accountUnits, err := runtime.service.ListOrganizationMemberUnitsForUser(runtime.ctx, owner.ID, organization.ID, account.ID)
	require.NoError(t, err)
	require.Len(t, accountUnits, 1)
	require.Equal(t, units[0].ID, accountUnits[0].UnitID)
	require.True(t, accountUnits[0].IsPrimary)
	require.Equal(t, "General", accountUnits[0].UnitName)
}

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

	runtime := startOrganizationServiceRuntime(t)
	owner := bootstrapOrganizationAdmin(t, runtime, "owner@example.com", "owner")
	target := createVerifiedOrganizationUser(t, runtime, "target@example.com", "target")

	organization, err := runtime.service.CreateOrganizationForUser(runtime.ctx, owner, "Acme Org", "acme-org")
	require.NoError(t, err)
	account, err := runtime.service.CreateOrganizationMemberForUser(runtime.ctx, owner.ID, organization.ID, "", target.Email, "member")
	require.NoError(t, err)

	engineering, err := runtime.service.CreateOrganizationUnitForUser(runtime.ctx, owner.ID, organization.ID, CreateOrganizationUnitParams{
		Name:            "Engineering",
		Slug:            "engineering",
		ManagerMemberID: account.ID,
	})
	require.NoError(t, err)
	require.Equal(t, account.ID, engineering.ManagerMemberID)

	platform, err := runtime.service.CreateOrganizationUnitForUser(runtime.ctx, owner.ID, organization.ID, CreateOrganizationUnitParams{
		Name:         "Platform",
		Slug:         "platform",
		ParentUnitID: engineering.ID,
	})
	require.NoError(t, err)
	require.Equal(t, engineering.ID, platform.ParentUnitID)

	secondaryMembership, err := runtime.service.AddOrganizationUnitMemberForUser(runtime.ctx, owner.ID, organization.ID, engineering.ID, AddOrganizationUnitMemberParams{
		MemberID: account.ID,
		Primary:  false,
	})
	require.NoError(t, err)
	require.False(t, secondaryMembership.IsPrimary)

	primaryMembership, err := runtime.service.AddOrganizationUnitMemberForUser(runtime.ctx, owner.ID, organization.ID, platform.ID, AddOrganizationUnitMemberParams{
		MemberID: account.ID,
		Primary:  true,
	})
	require.NoError(t, err)
	require.True(t, primaryMembership.IsPrimary)
	require.Equal(t, platform.ID, primaryMembership.PrimaryUnitID)

	engineeringAccounts, err := runtime.service.ListOrganizationUnitMembersForUser(runtime.ctx, owner.ID, organization.ID, engineering.ID)
	require.NoError(t, err)
	require.Len(t, engineeringAccounts, 1)
	require.False(t, engineeringAccounts[0].IsPrimary)
	require.Equal(t, platform.ID, engineeringAccounts[0].PrimaryUnitID)
	require.Equal(t, "Platform", engineeringAccounts[0].PrimaryUnitName)

	accountUnits, err := runtime.service.ListOrganizationMemberUnitsForUser(runtime.ctx, owner.ID, organization.ID, account.ID)
	require.NoError(t, err)
	require.Len(t, accountUnits, 3)
	require.Equal(t, platform.ID, accountUnits[0].UnitID)
	require.True(t, accountUnits[0].IsPrimary)

	err = runtime.service.RemoveOrganizationUnitMemberForUser(runtime.ctx, owner.ID, organization.ID, platform.ID, account.ID)
	require.ErrorIs(t, err, storeerr.ErrBadRequest)

	_, err = runtime.service.AddOrganizationUnitMemberForUser(runtime.ctx, owner.ID, organization.ID, engineering.ID, AddOrganizationUnitMemberParams{
		MemberID: account.ID,
		Primary:  true,
	})
	require.NoError(t, err)
	require.NoError(t, runtime.service.RemoveOrganizationUnitMemberForUser(runtime.ctx, owner.ID, organization.ID, platform.ID, account.ID))

	accountUnits, err = runtime.service.ListOrganizationMemberUnitsForUser(runtime.ctx, owner.ID, organization.ID, account.ID)
	require.NoError(t, err)
	require.Len(t, accountUnits, 2)
	require.Equal(t, engineering.ID, accountUnits[0].UnitID)
	require.True(t, accountUnits[0].IsPrimary)
}

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

	runtime := startOrganizationServiceRuntime(t)
	owner := bootstrapOrganizationAdmin(t, runtime, "owner@example.com", "owner")
	target := createVerifiedOrganizationUser(t, runtime, "target@example.com", "target")

	organization, err := runtime.service.CreateOrganizationForUser(runtime.ctx, owner, "Acme Org", "acme-org")
	require.NoError(t, err)
	account, err := runtime.service.CreateOrganizationMemberForUser(runtime.ctx, owner.ID, organization.ID, "", target.Email, "member")
	require.NoError(t, err)

	workspaceSummary, err := runtime.service.CreateOrganizationWorkspaceForUser(runtime.ctx, owner.ID, organization.ID, "Project One", "project-one")
	require.NoError(t, err)

	engineering, err := runtime.service.CreateOrganizationUnitForUser(runtime.ctx, owner.ID, organization.ID, CreateOrganizationUnitParams{
		Name: "Engineering",
		Slug: "engineering",
	})
	require.NoError(t, err)
	_, err = runtime.service.AddOrganizationUnitMemberForUser(runtime.ctx, owner.ID, organization.ID, engineering.ID, AddOrganizationUnitMemberParams{
		MemberID: account.ID,
	})
	require.NoError(t, err)

	var role string
	var hasAccess bool
	err = coredb.WithTx(runtime.ctx, runtime.service.db, func(tx coredb.Tx) error {
		var err error
		role, hasAccess, err = runtime.service.InTx(tx).loadEffectiveOrganizationWorkspaceRole(runtime.ctx, tx, organization.ID, workspaceSummary.ID, account.ID)
		return err
	})
	require.NoError(t, err)
	require.False(t, hasAccess)
	require.Empty(t, role)
}

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

	runtime := startOrganizationServiceRuntime(t)
	owner := bootstrapOrganizationAdmin(t, runtime, "owner@example.com", "owner")
	target := createVerifiedOrganizationUser(t, runtime, "target@example.com", "target")

	organization, err := runtime.service.CreateOrganizationForUser(runtime.ctx, owner, "Acme Org", "acme-org")
	require.NoError(t, err)
	account, err := runtime.service.CreateOrganizationMemberForUser(runtime.ctx, owner.ID, organization.ID, "", target.Email, "member")
	require.NoError(t, err)

	engineering, err := runtime.service.CreateOrganizationUnitForUser(runtime.ctx, owner.ID, organization.ID, CreateOrganizationUnitParams{
		Name: "Engineering",
		Slug: "engineering",
	})
	require.NoError(t, err)

	unitTeam, err := runtime.service.EnsureOrganizationUnitBackedTeamForUser(runtime.ctx, owner.ID, organization.ID, engineering.ID)
	require.NoError(t, err)
	require.Equal(t, "unit_backed", unitTeam.Source)

	teamAccounts, err := runtime.service.ListOrganizationTeamMembersForUser(runtime.ctx, owner.ID, organization.ID, unitTeam.ID)
	require.NoError(t, err)
	require.Empty(t, teamAccounts)

	_, err = runtime.service.AddOrganizationUnitMemberForUser(runtime.ctx, owner.ID, organization.ID, engineering.ID, AddOrganizationUnitMemberParams{
		MemberID: account.ID,
	})
	require.NoError(t, err)

	teamAccounts, err = runtime.service.ListOrganizationTeamMembersForUser(runtime.ctx, owner.ID, organization.ID, unitTeam.ID)
	require.NoError(t, err)
	require.Len(t, teamAccounts, 1)
	require.Equal(t, account.ID, teamAccounts[0].MemberID)
	require.Equal(t, "derived", teamAccounts[0].Source)
	require.False(t, teamAccounts[0].IsPrimary)

	_, err = runtime.service.AddOrganizationTeamMemberForUser(runtime.ctx, owner.ID, organization.ID, unitTeam.ID, AddOrganizationTeamMemberParams{
		MemberID: account.ID,
	})
	require.ErrorIs(t, err, storeerr.ErrBadRequest)

	require.NoError(t, runtime.service.RemoveOrganizationUnitMemberForUser(runtime.ctx, owner.ID, organization.ID, engineering.ID, account.ID))

	teamAccounts, err = runtime.service.ListOrganizationTeamMembersForUser(runtime.ctx, owner.ID, organization.ID, unitTeam.ID)
	require.NoError(t, err)
	require.Empty(t, teamAccounts)
}

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

	runtime := startOrganizationServiceRuntime(t)
	owner := bootstrapOrganizationAdmin(t, runtime, "owner@example.com", "owner")
	target := createVerifiedOrganizationUser(t, runtime, "target@example.com", "target")

	organization, err := runtime.service.CreateOrganizationForUser(runtime.ctx, owner, "Acme Org", "acme-org")
	require.NoError(t, err)
	account, err := runtime.service.CreateOrganizationMemberForUser(runtime.ctx, owner.ID, organization.ID, "", target.Email, "member")
	require.NoError(t, err)

	engineering, err := runtime.service.CreateOrganizationUnitForUser(runtime.ctx, owner.ID, organization.ID, CreateOrganizationUnitParams{
		Name: "Engineering",
		Slug: "engineering",
	})
	require.NoError(t, err)
	_, err = runtime.service.AddOrganizationUnitMemberForUser(runtime.ctx, owner.ID, organization.ID, engineering.ID, AddOrganizationUnitMemberParams{
		MemberID: account.ID,
	})
	require.NoError(t, err)

	unitTeam, err := runtime.service.EnsureOrganizationUnitBackedTeamForUser(runtime.ctx, owner.ID, organization.ID, engineering.ID)
	require.NoError(t, err)

	workspaceSummary, err := runtime.service.CreateOrganizationWorkspaceForUser(runtime.ctx, owner.ID, organization.ID, "Project One", "project-one")
	require.NoError(t, err)
	_, err = runtime.service.GrantOrganizationTeamWorkspaceAccessForUser(runtime.ctx, owner.ID, organization.ID, workspaceSummary.ID, unitTeam.ID, "member")
	require.NoError(t, err)

	workspaces, err := runtime.store.Workspace.ListUserWorkspaces(runtime.ctx, target.ID)
	require.NoError(t, err)
	require.True(t, slices.ContainsFunc(workspaces, func(item workspace.Summary) bool {
		return item.WorkspaceID == workspaceSummary.ID
	}))

	var directGrantCount int
	require.NoError(t, runtime.pool.QueryRow(runtime.ctx, `
SELECT COUNT(*)
FROM organization_workspace_member_grants
WHERE organization_id = $1
  AND workspace_id = $2
  AND organization_member_id = $3
  AND status = 'active'
`, organization.ID, workspaceSummary.ID, account.ID).Scan(&directGrantCount))
	require.Zero(t, directGrantCount)

	require.NoError(t, runtime.service.RemoveOrganizationUnitMemberForUser(runtime.ctx, owner.ID, organization.ID, engineering.ID, account.ID))

	workspaces, err = runtime.store.Workspace.ListUserWorkspaces(runtime.ctx, target.ID)
	require.NoError(t, err)
	require.False(t, slices.ContainsFunc(workspaces, func(item workspace.Summary) bool {
		return item.WorkspaceID == workspaceSummary.ID
	}))
}

func startOrganizationServiceRuntime(t *testing.T) serviceTestRuntime {
	t.Helper()

	return startOrganizationServiceRuntimeWithOptions(t, ServiceOptions{})
}

func startOrganizationServiceRuntimeWithOptions(t *testing.T, options ServiceOptions) serviceTestRuntime {
	t.Helper()
	ctx := context.Background()
	pool := startOrganizationRollbackDB(t, ctx)

	archiveStorage, err := archive.NewFilesystem(t.TempDir())
	require.NoError(t, err)

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	dataStore := store.New(pool, archiveStorage, nil, 30)
	service := NewWithDBOptions(pool, organizationTestCoreBridge{pool: pool, Store: dataStore}, options)
	require.NotNil(t, service)
	_ = logger

	return serviceTestRuntime{
		ctx:     ctx,
		pool:    pool,
		store:   dataStore,
		service: service,
	}
}

func bootstrapOrganizationAdmin(t *testing.T, runtime serviceTestRuntime, email string, name string) model.User {
	t.Helper()

	authenticated, err := runtime.store.Identity.BootstrapInitialAdmin(runtime.ctx, email, name, name, "hash", "token", time.Now().Add(24*time.Hour))
	require.NoError(t, err)
	return authenticated.User
}

func createVerifiedOrganizationUser(t *testing.T, runtime serviceTestRuntime, email string, name string) model.User {
	t.Helper()

	user, err := runtime.store.Identity.CreateVerifiedUser(runtime.ctx, email, name, name, "hash")
	require.NoError(t, err)
	return user
}

func createMobileVerifiedOrganizationUser(t *testing.T, runtime serviceTestRuntime, email string, name string, mobile string) model.User {
	t.Helper()

	user, err := runtime.store.Identity.RegisterUser(runtime.ctx, email, name, name, "hash", secrets.HashToken("unused-email-token"), time.Now().UTC().Add(time.Hour))
	require.NoError(t, err)

	code := "246810"
	require.NoError(t, runtime.store.Identity.CreateMobileVerificationToken(runtime.ctx, user.ID, mobile, secrets.HashToken(code), time.Now().UTC().Add(time.Hour)))

	verifiedUser, err := runtime.store.Identity.VerifyMobileToken(runtime.ctx, user.ID, code)
	require.NoError(t, err)
	return verifiedUser
}
