package team

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

func TestAgentTeamReadsRequireCurrentMemberOfActiveTeam(t *testing.T) {
	ctx := context.Background()
	db := startTeamTestPool(t, ctx)
	fixture := seedTeamStoreFixture(t, ctx, db)
	store := New(db)
	now := time.Date(2026, 7, 29, 9, 0, 0, 0, time.UTC)

	active, err := store.CreateTeam(ctx, CreateTeamParams{
		ID: "team_agent_read", WorkspaceID: fixture.workspaceID,
		Handle: "agent-read", DisplayName: "Agent Read", Description: "Visible",
		CreatedByMemberID: fixture.ownerMemberID,
		InitialMembers:    initialTeamLeader(fixture.agentMemberID),
		Now:               now,
	})
	require.NoError(t, err)
	principal := agent.AuthenticatedAgent{
		AgentID: "agt_not_the_member_key", WorkspaceID: fixture.workspaceID, MemberID: fixture.agentMemberID,
	}

	listed, err := store.ListTeamsForAgent(ctx, principal, "", 100)
	require.NoError(t, err)
	require.Len(t, listed, 1)
	require.Equal(t, active.ID, listed[0].ID)
	require.Equal(t, MemberRoleLeader, listed[0].Role)
	_, _, err = store.GetTeamForAgent(ctx, principal, active.ID)
	require.NoError(t, err)

	secondAgentMemberID := "mem_team_agent_read_second"
	insertTeamTestAgentMember(t, ctx, db, fixture, secondAgentMemberID, "Second Team Agent", now)
	_, err = store.AddTeamMember(ctx, AddTeamMemberParams{
		WorkspaceID: fixture.workspaceID, TeamID: active.ID,
		ActorKind: ActorKindAgent, ActorID: secondAgentMemberID, Role: MemberRoleLeader,
		AddedByMemberID: fixture.ownerMemberID, Now: now.Add(time.Minute),
	})
	require.NoError(t, err)
	members, err := store.ListActiveTeamMembers(ctx, fixture.workspaceID, active.ID)
	require.NoError(t, err)
	require.Len(t, members, 2)
	var testedTeamMemberID string
	for _, member := range members {
		if member.ActorID == fixture.agentMemberID {
			testedTeamMemberID = member.ID
		}
	}
	require.NotEmpty(t, testedTeamMemberID)
	require.NoError(t, store.RemoveTeamMember(ctx, fixture.workspaceID, active.ID, testedTeamMemberID, now.Add(2*time.Minute)))
	listed, err = store.ListTeamsForAgent(ctx, principal, "", 100)
	require.NoError(t, err)
	require.Empty(t, listed)
	_, _, err = store.GetTeamForAgent(ctx, principal, active.ID)
	require.Error(t, err)

	archived, err := store.CreateTeam(ctx, CreateTeamParams{
		ID: "team_agent_archived", WorkspaceID: fixture.workspaceID,
		Handle: "agent-archived", DisplayName: "Agent Archived",
		CreatedByMemberID: fixture.ownerMemberID,
		InitialMembers:    initialTeamLeader(fixture.agentMemberID),
		Now:               now.Add(2 * time.Minute),
	})
	require.NoError(t, err)
	require.NoError(t, store.DeleteTeam(ctx, fixture.workspaceID, archived.ID))
	listed, err = store.ListTeamsForAgent(ctx, principal, "", 100)
	require.NoError(t, err)
	require.Empty(t, listed)
	_, _, err = store.GetTeamForAgent(ctx, principal, archived.ID)
	require.Error(t, err)
}

type teamStoreFixture struct {
	workspaceID    string
	ownerUserID    string
	ownerMemberID  string
	memberUserID   string
	memberMemberID string
	agentMemberID  string
}

func boolPtr(value bool) *bool {
	return &value
}

func initialTeamLeader(actorID string) []AddTeamMemberParams {
	return initialTeamLeaderWithID("", actorID)
}

func initialTeamLeaderWithID(id string, actorID string) []AddTeamMemberParams {
	return []AddTeamMemberParams{{
		ID:        id,
		ActorKind: ActorKindAgent,
		ActorID:   actorID,
		Role:      MemberRoleLeader,
	}}
}

func TestCreateTeamCreatesOnlyAgentAssignmentsAndDeleteReleasesHandle(t *testing.T) {
	ctx := context.Background()
	db := startTeamTestPool(t, ctx)
	fixture := seedTeamStoreFixture(t, ctx, db)
	store := New(db)
	now := time.Date(2026, 6, 18, 9, 0, 0, 0, time.UTC)

	created, err := store.CreateTeam(ctx, CreateTeamParams{
		ID:                "team_core",
		WorkspaceID:       fixture.workspaceID,
		Handle:            "Core-Team",
		DisplayName:       "Core Team",
		Description:       "Team that owns core work",
		Visibility:        VisibilityPrivate,
		CreatedByMemberID: fixture.ownerMemberID,
		InitialMembers: []AddTeamMemberParams{{
			ActorKind:         ActorKindAgent,
			ActorID:           fixture.agentMemberID,
			Role:              MemberRoleLeader,
			RoleLabel:         "Reviewer",
			RoleOverlayPrompt: "Review changes for this team.",
		}},
		Now: now,
	})
	require.NoError(t, err)
	require.Equal(t, "core-team", created.Handle)
	require.Equal(t, VisibilityPrivate, created.Visibility)
	require.Equal(t, StatusActive, created.Status)

	members, err := store.ListActiveTeamMembers(ctx, fixture.workspaceID, created.ID)
	require.NoError(t, err)
	require.Len(t, members, 1)
	require.Equal(t, ActorKindAgent, members[0].ActorKind)
	require.Equal(t, fixture.agentMemberID, members[0].ActorID)
	require.Equal(t, MemberRoleLeader, members[0].Role)
	require.Equal(t, "Reviewer", members[0].RoleLabel)
	require.Equal(t, "Review changes for this team.", members[0].RoleOverlayPrompt)
	require.True(t, members[0].WakeOnTeamMention)

	loaded, err := store.LoadTeamByHandle(ctx, fixture.workspaceID, "core-team")
	require.NoError(t, err)
	require.Equal(t, created.ID, loaded.ID)

	_, err = store.CreateTeam(ctx, CreateTeamParams{
		ID:                "team_duplicate",
		WorkspaceID:       fixture.workspaceID,
		Handle:            "core-team",
		DisplayName:       "Duplicate",
		CreatedByMemberID: fixture.ownerMemberID,
		Now:               now,
	})
	require.ErrorIs(t, err, storeerr.ErrConflict)

	require.NoError(t, store.DeleteTeam(ctx, fixture.workspaceID, created.ID))

	reused, err := store.CreateTeam(ctx, CreateTeamParams{
		ID:                "team_reused",
		WorkspaceID:       fixture.workspaceID,
		Handle:            "core-team",
		DisplayName:       "Reused Handle",
		CreatedByMemberID: fixture.ownerMemberID,
		InitialMembers:    initialTeamLeader(fixture.agentMemberID),
		Now:               now.Add(2 * time.Hour),
	})
	require.NoError(t, err)
	require.Equal(t, "team_reused", reused.ID)
}

func TestCreateTeamDefaultsFirstAgentToLeader(t *testing.T) {
	ctx := context.Background()
	db := startTeamTestPool(t, ctx)
	fixture := seedTeamStoreFixture(t, ctx, db)
	store := New(db)

	created, err := store.CreateTeam(ctx, CreateTeamParams{
		ID:                "team_default_leader",
		WorkspaceID:       fixture.workspaceID,
		Handle:            "default-leader",
		DisplayName:       "Default Leader",
		CreatedByMemberID: fixture.ownerMemberID,
		InitialMembers: []AddTeamMemberParams{{
			ActorKind: ActorKindAgent,
			ActorID:   fixture.agentMemberID,
		}},
	})
	require.NoError(t, err)

	members, err := store.ListActiveTeamMembers(ctx, fixture.workspaceID, created.ID)
	require.NoError(t, err)
	require.Len(t, members, 1)
	require.Equal(t, MemberRoleLeader, members[0].Role)
	require.True(t, members[0].WakeOnTeamMention)
}

func TestCreateTeamDefaultsDeduplicatedFirstAgentToLeader(t *testing.T) {
	ctx := context.Background()
	db := startTeamTestPool(t, ctx)
	fixture := seedTeamStoreFixture(t, ctx, db)
	store := New(db)

	created, err := store.CreateTeam(ctx, CreateTeamParams{
		ID:                "team_deduplicated_default_leader",
		WorkspaceID:       fixture.workspaceID,
		Handle:            "deduplicated-default-leader",
		DisplayName:       "Deduplicated Default Leader",
		CreatedByMemberID: fixture.ownerMemberID,
		InitialMembers: []AddTeamMemberParams{
			{ActorKind: ActorKindAgent, ActorID: fixture.agentMemberID},
			{ActorKind: ActorKindAgent, ActorID: fixture.agentMemberID, Role: MemberRoleLeader},
		},
	})
	require.NoError(t, err)

	members, err := store.ListActiveTeamMembers(ctx, fixture.workspaceID, created.ID)
	require.NoError(t, err)
	require.Len(t, members, 1)
	require.Equal(t, MemberRoleLeader, members[0].Role)
}

func TestTeamChannelLifecycleSyncsMembersAndProfile(t *testing.T) {
	ctx := context.Background()
	db := startTeamTestPool(t, ctx)
	fixture := seedTeamStoreFixture(t, ctx, db)
	store := New(db)
	now := time.Date(2026, 6, 18, 9, 30, 0, 0, time.UTC)
	secondAgentMemberID := "mem_team_channel_agent_two"
	insertTeamTestAgentMember(t, ctx, db, fixture, secondAgentMemberID, "Team Channel Agent Two", now)

	created, err := store.CreateTeam(ctx, CreateTeamParams{
		ID:                "team_channel",
		WorkspaceID:       fixture.workspaceID,
		Handle:            "channel-team",
		DisplayName:       "Channel Team",
		Description:       "Initial team channel description",
		CreatedByMemberID: fixture.ownerMemberID,
		InitialMembers: []AddTeamMemberParams{{
			ID:        "wtm_team_channel_first",
			ActorKind: ActorKindAgent,
			ActorID:   fixture.agentMemberID,
			Role:      MemberRoleLeader,
		}},
		Now: now,
	})
	require.NoError(t, err)
	require.NotEmpty(t, created.ChannelID)

	var profileSlug, profileName, profileDescription, profileSubsystem, profileOwner string
	require.NoError(t, db.QueryRow(ctx, `
SELECT slug, display_name, description, subsystem, COALESCE(owner_member_id, '')
FROM scope_channel_profiles
WHERE workspace_id = $1 AND scope_id = $2
`, fixture.workspaceID, created.ChannelID).Scan(&profileSlug, &profileName, &profileDescription, &profileSubsystem, &profileOwner))
	require.Equal(t, created.ID, profileSlug)
	require.Equal(t, "Channel Team", profileName)
	require.Equal(t, "Initial team channel description", profileDescription)
	require.Equal(t, "team", profileSubsystem)
	require.Equal(t, fixture.ownerMemberID, profileOwner)

	assertTeamChannelMember := func(memberID string, wantState string) {
		t.Helper()
		var membershipCount int
		require.NoError(t, db.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
  AND visibility = 'public'
  AND removed_at IS NULL
`, created.ChannelID, memberID).Scan(&membershipCount))
		require.Equal(t, 1, membershipCount)
		var state string
		require.NoError(t, db.QueryRow(ctx, `
SELECT state
FROM scope_notification_settings
WHERE scope_id = $1 AND member_id = $2
`, created.ChannelID, memberID).Scan(&state))
		require.Equal(t, wantState, state)
	}
	assertTeamChannelMember(fixture.ownerMemberID, "active")
	assertTeamChannelMember(fixture.agentMemberID, "auto-wake")

	channels, err := message.ListChannelsForMember(ctx, db, fixture.ownerMemberID, fixture.workspaceID)
	require.NoError(t, err)
	var listedTeamChannel *message.ChannelSummary
	for idx := range channels {
		if channels[idx].ChannelID == created.ChannelID {
			listedTeamChannel = &channels[idx]
			break
		}
	}
	require.NotNil(t, listedTeamChannel, "joined team subsystem channels must be returned so the web channel-access list is complete")
	require.Equal(t, message.ChannelSubsystemTeam, listedTeamChannel.Subsystem)
	require.Equal(t, created.ID, listedTeamChannel.Slug)
	require.Equal(t, created.ID, listedTeamChannel.TeamID)
	require.Equal(t, created.Handle, listedTeamChannel.TeamHandle)
	require.Equal(t, created.DisplayName, listedTeamChannel.TeamDisplayName)
	require.True(t, listedTeamChannel.Joined)
	require.False(t, listedTeamChannel.CanInvite)
	require.False(t, listedTeamChannel.CanManage)

	added, err := store.AddTeamMember(ctx, AddTeamMemberParams{
		ID:              "wtm_team_channel_second",
		WorkspaceID:     fixture.workspaceID,
		TeamID:          created.ID,
		ActorKind:       ActorKindAgent,
		ActorID:         secondAgentMemberID,
		AddedByMemberID: fixture.ownerMemberID,
		Now:             now.Add(time.Minute),
	})
	require.NoError(t, err)
	assertTeamChannelMember(secondAgentMemberID, "auto-wake")

	renamed := "Renamed Team Channel"
	description := "Synced team channel description"
	updated, err := store.UpdateTeam(ctx, fixture.workspaceID, created.ID, UpdateTeamParams{
		DisplayName: &renamed,
		Description: &description,
		Now:         now.Add(2 * time.Minute),
	})
	require.NoError(t, err)
	require.Equal(t, created.ChannelID, updated.ChannelID)
	require.NoError(t, db.QueryRow(ctx, `
SELECT display_name, description
FROM scope_channel_profiles
WHERE workspace_id = $1 AND scope_id = $2
`, fixture.workspaceID, created.ChannelID).Scan(&profileName, &profileDescription))
	require.Equal(t, renamed, profileName)
	require.Equal(t, description, profileDescription)

	require.NoError(t, store.RemoveTeamMember(ctx, fixture.workspaceID, created.ID, added.ID, now.Add(3*time.Minute)))
	var removedMembershipCount int
	require.NoError(t, db.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id = $1 AND member_id = $2 AND removed_at IS NOT NULL
`, created.ChannelID, secondAgentMemberID).Scan(&removedMembershipCount))
	require.Equal(t, 1, removedMembershipCount)
	var notificationCount int
	require.NoError(t, db.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_notification_settings
WHERE scope_id = $1 AND member_id = $2
`, created.ChannelID, secondAgentMemberID).Scan(&notificationCount))
	require.Zero(t, notificationCount)

	require.NoError(t, store.DeleteTeam(ctx, fixture.workspaceID, created.ID))
	var channelStatus, archivedSlug string
	require.NoError(t, db.QueryRow(ctx, `
SELECT s.status, p.slug
FROM scopes s
JOIN scope_channel_profiles p ON p.scope_id = s.id
WHERE s.id = $1
`, created.ChannelID).Scan(&channelStatus, &archivedSlug))
	require.Equal(t, "archived", channelStatus)
	require.NotEqual(t, profileSlug, archivedSlug)
}

func TestTeamChannelPreferencesSurviveReconcile(t *testing.T) {
	ctx := context.Background()
	db := startTeamTestPool(t, ctx)
	fixture := seedTeamStoreFixture(t, ctx, db)
	store := New(db)
	now := time.Date(2026, 8, 11, 5, 0, 0, 0, time.UTC)

	created, err := store.CreateTeam(ctx, CreateTeamParams{
		ID:                "team_pref_survive",
		WorkspaceID:       fixture.workspaceID,
		Handle:            "pref-survive",
		DisplayName:       "Preference Survive Team",
		CreatedByMemberID: fixture.ownerMemberID,
		InitialMembers:    initialTeamLeader(fixture.agentMemberID),
		Now:               now,
	})
	require.NoError(t, err)

	assertState := func(memberID string, want string) {
		t.Helper()
		var state string
		require.NoError(t, db.QueryRow(ctx, `
SELECT state
FROM scope_notification_settings
WHERE scope_id = $1 AND member_id = $2
`, created.ChannelID, memberID).Scan(&state))
		require.Equal(t, want, state)
	}
	loadGeneration := func(memberID string) int64 {
		t.Helper()
		var generation int64
		require.NoError(t, db.QueryRow(ctx, `
SELECT unread_generation
FROM scope_notification_settings
WHERE scope_id = $1 AND member_id = $2
`, created.ChannelID, memberID).Scan(&generation))
		return generation
	}

	require.NoError(t, message.UpsertScopeNotificationState(
		ctx, db, fixture.ownerMemberID, created.ChannelID,
		message.HumanScopeSubscriptionStateMute, now.Add(time.Minute),
	))
	require.NoError(t, message.UpsertScopeNotificationState(
		ctx, db, fixture.agentMemberID, created.ChannelID,
		"no-wake", now.Add(time.Minute),
	))
	ownerGeneration := loadGeneration(fixture.ownerMemberID)
	agentGeneration := loadGeneration(fixture.agentMemberID)

	_, err = store.EnsureTeamChannel(ctx, fixture.workspaceID, created.ID)
	require.NoError(t, err)
	assertState(fixture.ownerMemberID, "mute")
	assertState(fixture.agentMemberID, "no-wake")
	require.Equal(t, ownerGeneration, loadGeneration(fixture.ownerMemberID))
	require.Equal(t, agentGeneration, loadGeneration(fixture.agentMemberID))

	require.NoError(t, message.UpsertScopeNotificationState(
		ctx, db, fixture.ownerMemberID, created.ChannelID,
		message.HumanScopeSubscriptionStateQuiet, now.Add(2*time.Minute),
	))
	ownerGeneration = loadGeneration(fixture.ownerMemberID)
	renamed := "Preference Survive Renamed"
	_, err = store.UpdateTeam(ctx, fixture.workspaceID, created.ID, UpdateTeamParams{
		DisplayName: &renamed,
		Now:         now.Add(3 * time.Minute),
	})
	require.NoError(t, err)
	assertState(fixture.ownerMemberID, "quiet")
	assertState(fixture.agentMemberID, "no-wake")
	require.Equal(t, ownerGeneration, loadGeneration(fixture.ownerMemberID))
	require.Equal(t, agentGeneration, loadGeneration(fixture.agentMemberID))

	secondAgentMemberID := "mem_team_pref_survive_second"
	insertTeamTestAgentMember(t, ctx, db, fixture, secondAgentMemberID, "Preference Survive Agent Two", now)
	_, err = store.AddTeamMember(ctx, AddTeamMemberParams{
		ID:              "wtm_team_pref_survive_second",
		WorkspaceID:     fixture.workspaceID,
		TeamID:          created.ID,
		ActorKind:       ActorKindAgent,
		ActorID:         secondAgentMemberID,
		AddedByMemberID: fixture.ownerMemberID,
		Now:             now.Add(4 * time.Minute),
	})
	require.NoError(t, err)
	assertState(fixture.ownerMemberID, "quiet")
	assertState(fixture.agentMemberID, "no-wake")
	assertState(secondAgentMemberID, "auto-wake")
	require.Equal(t, ownerGeneration, loadGeneration(fixture.ownerMemberID))
	require.Equal(t, agentGeneration, loadGeneration(fixture.agentMemberID))
}

func TestTeamChannelMembersCanOpenReservedReplyThreads(t *testing.T) {
	ctx := context.Background()
	db := startTeamTestPool(t, ctx)
	fixture := seedTeamStoreFixture(t, ctx, db)
	store := New(db)
	now := time.Date(2026, 8, 2, 4, 0, 0, 0, time.UTC)

	created, err := store.CreateTeam(ctx, CreateTeamParams{
		ID:                "team_reserved_reply_thread",
		WorkspaceID:       fixture.workspaceID,
		Handle:            "reserved-reply-thread",
		DisplayName:       "Reserved Reply Thread",
		CreatedByMemberID: fixture.ownerMemberID,
		InitialMembers:    initialTeamLeader(fixture.agentMemberID),
		Now:               now,
	})
	require.NoError(t, err)
	require.NotEmpty(t, created.ChannelID)

	const (
		ownerParentMessage = "a1b2c3d4e5f60718293a4b5c6d7e8f90"
		agentParentMessage = "0f1e2d3c4b5a69788796a5b4c3d2e1f0"
		guestParentMessage = "11223344556677889900aabbccddeeff"
		ownerlessParent    = "ffeeddccbbaa00998877665544332211"
	)

	ownerThread, err := message.OpenReplyThreadForReservedParentMessage(
		ctx, db, fixture.ownerMemberID, fixture.workspaceID, created.ChannelID, ownerParentMessage,
	)
	require.NoError(t, err)
	require.NotEmpty(t, ownerThread.ThreadChannelID)

	agentThread, err := message.OpenReplyThreadForReservedParentMessage(
		ctx, db, fixture.agentMemberID, fixture.workspaceID, created.ChannelID, agentParentMessage,
	)
	require.NoError(t, err)
	require.NotEmpty(t, agentThread.ThreadChannelID)

	_, err = message.OpenReplyThreadForReservedParentMessage(
		ctx, db, fixture.memberMemberID, fixture.workspaceID, created.ChannelID, guestParentMessage,
	)
	require.ErrorIs(t, err, message.ErrNotFound)

	const ownerlessChannelID = "chn_team_test_ownerless_im"
	insertTeamTestChannel(t, ctx, db, fixture.workspaceID, ownerlessChannelID, fixture.ownerMemberID, now)
	mustExecTeamTest(t, ctx, db, `
UPDATE scope_channel_profiles
SET owner_member_id = NULL
WHERE scope_id = $1
`, ownerlessChannelID)
	_, err = message.OpenReplyThreadForReservedParentMessage(
		ctx, db, fixture.ownerMemberID, fixture.workspaceID, ownerlessChannelID, ownerlessParent,
	)
	require.ErrorIs(t, err, message.ErrNotFound)
}

func TestEnsureTeamChannelRepairsExistingMemberSync(t *testing.T) {
	ctx := context.Background()
	db := startTeamTestPool(t, ctx)
	fixture := seedTeamStoreFixture(t, ctx, db)
	store := New(db)
	now := time.Date(2026, 6, 18, 10, 30, 0, 0, time.UTC)

	created, err := store.CreateTeam(ctx, CreateTeamParams{
		ID:                "team_channel_repair",
		WorkspaceID:       fixture.workspaceID,
		Handle:            "channel-repair-team",
		DisplayName:       "Channel Repair Team",
		CreatedByMemberID: fixture.ownerMemberID,
		InitialMembers:    initialTeamLeaderWithID("wtm_team_channel_repair_agent", fixture.agentMemberID),
		Now:               now,
	})
	require.NoError(t, err)
	require.NotEmpty(t, created.ChannelID)

	mustExecTeamTest(t, ctx, db, `
DELETE FROM scope_memberships
WHERE scope_id = $1 AND member_id = $2
`, created.ChannelID, fixture.agentMemberID)
	mustExecTeamTest(t, ctx, db, `
DELETE FROM scope_notification_settings
WHERE scope_id = $1 AND member_id = $2
`, created.ChannelID, fixture.agentMemberID)

	var beforeRepairCount int
	require.NoError(t, db.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
  AND visibility = 'public'
  AND removed_at IS NULL
`, created.ChannelID, fixture.agentMemberID).Scan(&beforeRepairCount))
	require.Zero(t, beforeRepairCount)

	ensured, err := store.EnsureTeamChannel(ctx, fixture.workspaceID, created.ID)
	require.NoError(t, err)
	require.Equal(t, created.ChannelID, ensured.ChannelID)

	var membershipCount int
	require.NoError(t, db.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
  AND visibility = 'public'
  AND removed_at IS NULL
`, created.ChannelID, fixture.agentMemberID).Scan(&membershipCount))
	require.Equal(t, 1, membershipCount)

	var state string
	require.NoError(t, db.QueryRow(ctx, `
SELECT state
FROM scope_notification_settings
WHERE scope_id = $1 AND member_id = $2
`, created.ChannelID, fixture.agentMemberID).Scan(&state))
	require.Equal(t, "auto-wake", state)
}

func TestCreateTeamAllowsChineseHandleAndDisplayName(t *testing.T) {
	ctx := context.Background()
	db := startTeamTestPool(t, ctx)
	fixture := seedTeamStoreFixture(t, ctx, db)
	store := New(db)
	now := time.Date(2026, 6, 18, 10, 0, 0, 0, time.UTC)

	created, err := store.CreateTeam(ctx, CreateTeamParams{
		ID:                "team_cn",
		WorkspaceID:       fixture.workspaceID,
		Handle:            "功能小队",
		DisplayName:       "功能小队",
		CreatedByMemberID: fixture.ownerMemberID,
		InitialMembers:    initialTeamLeader(fixture.agentMemberID),
		Now:               now,
	})
	require.NoError(t, err)
	require.Equal(t, "功能小队", created.Handle)
	require.Equal(t, "功能小队", created.DisplayName)

	loaded, err := store.LoadTeamByHandle(ctx, fixture.workspaceID, "功能小队")
	require.NoError(t, err)
	require.Equal(t, created.ID, loaded.ID)
}

func TestAddTeamMemberOnlyAllowsAgentAssignments(t *testing.T) {
	ctx := context.Background()
	db := startTeamTestPool(t, ctx)
	fixture := seedTeamStoreFixture(t, ctx, db)
	store := New(db)
	now := time.Date(2026, 6, 18, 11, 0, 0, 0, time.UTC)
	secondAgentMemberID := "mem_team_validation_second"
	insertTeamTestAgentMember(t, ctx, db, fixture, secondAgentMemberID, "Validation Second Agent", now)

	created, err := store.CreateTeam(ctx, CreateTeamParams{
		ID:                "team_validation",
		WorkspaceID:       fixture.workspaceID,
		Handle:            "validation",
		DisplayName:       "Validation",
		CreatedByMemberID: fixture.ownerMemberID,
		InitialMembers:    initialTeamLeaderWithID("wtm_validation_leader", fixture.agentMemberID),
		Now:               now,
	})
	require.NoError(t, err)

	_, err = store.AddTeamMember(ctx, AddTeamMemberParams{
		ID:              "wtm_bad_kind",
		WorkspaceID:     fixture.workspaceID,
		TeamID:          created.ID,
		ActorKind:       ActorKindHuman,
		ActorID:         fixture.ownerMemberID,
		AddedByMemberID: fixture.ownerMemberID,
		Now:             now,
	})
	require.ErrorIs(t, err, storeerr.ErrBadRequest)

	doNotWakeOnMention := false
	_, err = store.AddTeamMember(ctx, AddTeamMemberParams{
		ID:                "wtm_agent",
		WorkspaceID:       fixture.workspaceID,
		TeamID:            created.ID,
		ActorKind:         ActorKindAgent,
		ActorID:           secondAgentMemberID,
		RoleLabel:         "Builder",
		RoleOverlayPrompt: "Build assigned work.",
		WakeOnTeamMention: &doNotWakeOnMention,
		AddedByMemberID:   fixture.ownerMemberID,
		Now:               now.Add(30 * time.Second),
	})
	require.NoError(t, err)
	member, err := store.LoadTeamMember(ctx, fixture.workspaceID, created.ID, "wtm_agent")
	require.NoError(t, err)
	require.Equal(t, ActorKindAgent, member.ActorKind)
	require.Equal(t, MemberRoleMember, member.Role)
	require.Equal(t, "Builder", member.RoleLabel)

	_, err = store.AddTeamMember(ctx, AddTeamMemberParams{
		ID:              "wtm_agent_duplicate",
		WorkspaceID:     fixture.workspaceID,
		TeamID:          created.ID,
		ActorKind:       ActorKindAgent,
		ActorID:         secondAgentMemberID,
		AddedByMemberID: fixture.ownerMemberID,
		Now:             now.Add(2 * time.Minute),
	})
	require.ErrorIs(t, err, storeerr.ErrConflict)

	updatedOverlay := "Review only critical paths."
	updated, err := store.UpdateTeamMember(ctx, fixture.workspaceID, created.ID, member.ID, UpdateTeamMemberParams{
		RoleOverlayPrompt: &updatedOverlay,
		Now:               now.Add(3 * time.Minute),
	})
	require.NoError(t, err)
	require.Equal(t, updatedOverlay, updated.RoleOverlayPrompt)
	require.Equal(t, int64(2), updated.RoleOverlayVersion)

	_, err = store.UpdateTeamMember(ctx, fixture.workspaceID, created.ID, "wtm_validation_leader", UpdateTeamMemberParams{
		WakeOnTeamMention: &doNotWakeOnMention,
		Now:               now.Add(3*time.Minute + 30*time.Second),
	})
	require.ErrorIs(t, err, storeerr.ErrBadRequest)

	require.NoError(t, store.RemoveTeamMember(ctx, fixture.workspaceID, created.ID, member.ID, now.Add(4*time.Minute)))
	removed, err := store.LoadTeamMember(ctx, fixture.workspaceID, created.ID, member.ID)
	require.NoError(t, err)
	require.NotNil(t, removed.RemovedAt)
}

func TestTeamMemberRolesAndAgentWorkspaceAssignment(t *testing.T) {
	ctx := context.Background()
	db := startTeamTestPool(t, ctx)
	fixture := seedTeamStoreFixture(t, ctx, db)
	store := New(db)
	now := time.Date(2026, 6, 18, 12, 0, 0, 0, time.UTC)
	secondAgentMemberID := "mem_team_agent_two"
	thirdAgentMemberID := "mem_team_agent_three"
	insertTeamTestAgentMember(t, ctx, db, fixture, secondAgentMemberID, "Second Agent", now)
	insertTeamTestAgentMember(t, ctx, db, fixture, thirdAgentMemberID, "Third Agent", now)

	created, err := store.CreateTeam(ctx, CreateTeamParams{
		ID:                "team_roles",
		WorkspaceID:       fixture.workspaceID,
		Handle:            "roles",
		DisplayName:       "Roles",
		CreatedByMemberID: fixture.ownerMemberID,
		InitialMembers: []AddTeamMemberParams{
			{
				ID:        "wtm_roles_first",
				ActorKind: ActorKindAgent,
				ActorID:   fixture.agentMemberID,
				Role:      MemberRoleLeader,
			},
			{
				ID:        "wtm_roles_second",
				ActorKind: ActorKindAgent,
				ActorID:   secondAgentMemberID,
			},
		},
		Now: now,
	})
	require.NoError(t, err)

	members, err := store.ListActiveTeamMembers(ctx, fixture.workspaceID, created.ID)
	require.NoError(t, err)
	require.Len(t, members, 2)
	require.Equal(t, MemberRoleLeader, members[0].Role)
	require.Equal(t, MemberRoleMember, members[1].Role)

	newLeaderRole := MemberRoleLeader
	wakeNewLeader := true
	updated, err := store.UpdateTeamMember(ctx, fixture.workspaceID, created.ID, "wtm_roles_second", UpdateTeamMemberParams{
		Role:              &newLeaderRole,
		WakeOnTeamMention: &wakeNewLeader,
		Now:               now.Add(time.Minute),
	})
	require.NoError(t, err)
	require.Equal(t, MemberRoleLeader, updated.Role)
	members, err = store.ListActiveTeamMembers(ctx, fixture.workspaceID, created.ID)
	require.NoError(t, err)
	require.Equal(t, MemberRoleMember, members[0].Role)
	require.Equal(t, MemberRoleLeader, members[1].Role)

	otherTeam, err := store.CreateTeam(ctx, CreateTeamParams{
		ID:                "team_roles_other",
		WorkspaceID:       fixture.workspaceID,
		Handle:            "roles-other",
		DisplayName:       "Roles Other",
		CreatedByMemberID: fixture.ownerMemberID,
		InitialMembers:    initialTeamLeader(thirdAgentMemberID),
		Now:               now.Add(2 * time.Minute),
	})
	require.NoError(t, err)
	_, err = store.AddTeamMember(ctx, AddTeamMemberParams{
		ID:              "wtm_roles_duplicate_workspace",
		WorkspaceID:     fixture.workspaceID,
		TeamID:          otherTeam.ID,
		ActorKind:       ActorKindAgent,
		ActorID:         fixture.agentMemberID,
		AddedByMemberID: fixture.ownerMemberID,
		Now:             now.Add(3 * time.Minute),
	})
	require.ErrorIs(t, err, storeerr.ErrConflict)

	require.NoError(t, store.RemoveTeamMember(ctx, fixture.workspaceID, created.ID, "wtm_roles_first", now.Add(4*time.Minute)))
	reassigned, err := store.AddTeamMember(ctx, AddTeamMemberParams{
		ID:              "wtm_roles_reassigned",
		WorkspaceID:     fixture.workspaceID,
		TeamID:          otherTeam.ID,
		ActorKind:       ActorKindAgent,
		ActorID:         fixture.agentMemberID,
		AddedByMemberID: fixture.ownerMemberID,
		Now:             now.Add(5 * time.Minute),
	})
	require.NoError(t, err)
	require.Equal(t, MemberRoleMember, reassigned.Role)
}

func TestAddTeamMemberDefaultsSortOrderAfterExistingMembers(t *testing.T) {
	ctx := context.Background()
	db := startTeamTestPool(t, ctx)
	fixture := seedTeamStoreFixture(t, ctx, db)
	store := New(db)
	now := time.Date(2026, 7, 9, 8, 0, 0, 0, time.UTC)
	secondAgentMemberID := "mem_team_sort_second"
	insertTeamTestAgentMember(t, ctx, db, fixture, secondAgentMemberID, "Sort Second Agent", now)

	created, err := store.CreateTeam(ctx, CreateTeamParams{
		ID:                "team_sort_defaults",
		WorkspaceID:       fixture.workspaceID,
		Handle:            "sort-defaults",
		DisplayName:       "Sort Defaults",
		CreatedByMemberID: fixture.ownerMemberID,
		InitialMembers:    initialTeamLeaderWithID("wtm_sort_leader", fixture.agentMemberID),
		Now:               now,
	})
	require.NoError(t, err)

	added, err := store.AddTeamMember(ctx, AddTeamMemberParams{
		ID:              "wtm_sort_second",
		WorkspaceID:     fixture.workspaceID,
		TeamID:          created.ID,
		ActorKind:       ActorKindAgent,
		ActorID:         secondAgentMemberID,
		AddedByMemberID: fixture.ownerMemberID,
		Now:             now,
	})
	require.NoError(t, err)
	require.Greater(t, added.SortOrder, int64(20))
	require.False(t, added.WakeOnTeamMention)

	members, err := store.ListTeamMembersForUser(ctx, fixture.ownerUserID, fixture.workspaceID, created.ID)
	require.NoError(t, err)
	require.Len(t, members, 2)
	require.Equal(t, fixture.agentMemberID, members[0].Member.ActorID)
	require.Equal(t, secondAgentMemberID, members[1].Member.ActorID)
}

func TestTeamDirectoryVisibilityUsesCreatorNotHumanMembers(t *testing.T) {
	ctx := context.Background()
	db := startTeamTestPool(t, ctx)
	fixture := seedTeamStoreFixture(t, ctx, db)
	store := New(db)
	now := time.Date(2026, 6, 18, 13, 0, 0, 0, time.UTC)
	privateAgentMemberID := "mem_team_directory_private"
	insertTeamTestAgentMember(t, ctx, db, fixture, privateAgentMemberID, "Directory Private Agent", now)

	workspaceTeam, err := store.CreateTeam(ctx, CreateTeamParams{
		ID:                "team_directory_workspace",
		WorkspaceID:       fixture.workspaceID,
		Handle:            "directory-workspace",
		DisplayName:       "Directory Workspace",
		Visibility:        VisibilityWorkspace,
		CreatedByMemberID: fixture.ownerMemberID,
		InitialMembers:    initialTeamLeader(fixture.agentMemberID),
		Now:               now,
	})
	require.NoError(t, err)
	privateTeam, err := store.CreateTeam(ctx, CreateTeamParams{
		ID:                "team_directory_private",
		WorkspaceID:       fixture.workspaceID,
		Handle:            "directory-private",
		DisplayName:       "Directory Private",
		Visibility:        VisibilityPrivate,
		CreatedByMemberID: fixture.ownerMemberID,
		InitialMembers:    initialTeamLeader(privateAgentMemberID),
		Now:               now.Add(time.Minute),
	})
	require.NoError(t, err)

	memberTeams, err := store.ListTeamsForUser(ctx, fixture.memberUserID, fixture.workspaceID, ListTeamsFilter{Status: "all"})
	require.NoError(t, err)
	require.Empty(t, teamViewIDs(memberTeams))

	ownerTeams, err := store.ListTeamsForUser(ctx, fixture.ownerUserID, fixture.workspaceID, ListTeamsFilter{Status: "all"})
	require.NoError(t, err)
	require.ElementsMatch(t, []string{workspaceTeam.ID, privateTeam.ID}, teamViewIDs(ownerTeams))

	memberCandidates, err := store.ListTeamMentionCandidatesForUser(ctx, fixture.memberUserID, fixture.workspaceID, "directory", 20, "")
	require.NoError(t, err)
	require.Empty(t, teamCandidateIDs(memberCandidates))

	ownerCandidates, err := store.ListTeamMentionCandidatesForUser(ctx, fixture.ownerUserID, fixture.workspaceID, "directory", 20, "")
	require.NoError(t, err)
	require.ElementsMatch(t, []string{workspaceTeam.ID, privateTeam.ID}, teamCandidateIDs(ownerCandidates))
}

func TestTeamMentionExpansionOnlyTargetsAgents(t *testing.T) {
	ctx := context.Background()
	db := startTeamTestPool(t, ctx)
	fixture := seedTeamStoreFixture(t, ctx, db)
	store := New(db)
	now := time.Date(2026, 6, 19, 9, 0, 0, 0, time.UTC)
	secondAgentMemberID := "mem_team_expansion_second"
	insertTeamTestAgentMember(t, ctx, db, fixture, secondAgentMemberID, "Expansion Second Agent", now)

	team, err := store.CreateTeam(ctx, CreateTeamParams{
		ID:                "team_expansion",
		WorkspaceID:       fixture.workspaceID,
		Handle:            "expansion",
		DisplayName:       "Expansion",
		Visibility:        VisibilityWorkspace,
		CreatedByMemberID: fixture.ownerMemberID,
		InitialMembers: []AddTeamMemberParams{{
			ID:                "wtm_expansion_agent",
			ActorKind:         ActorKindAgent,
			ActorID:           fixture.agentMemberID,
			Role:              MemberRoleLeader,
			RoleLabel:         "Builder",
			RoleOverlayPrompt: "Build visible work.",
		}},
		Now: now,
	})
	require.NoError(t, err)
	_, err = store.AddTeamMember(ctx, AddTeamMemberParams{
		ID:                "wtm_expansion_second_agent",
		WorkspaceID:       fixture.workspaceID,
		TeamID:            team.ID,
		ActorKind:         ActorKindAgent,
		ActorID:           secondAgentMemberID,
		RoleLabel:         "Researcher",
		WakeOnTeamMention: boolPtr(false),
		SortOrder:         20,
		AddedByMemberID:   fixture.ownerMemberID,
		Now:               now.Add(2 * time.Minute),
	})
	require.NoError(t, err)
	insertTeamTestMachineAndAgent(t, ctx, db, fixture, "mac_expansion", "agt_expansion", "expansion-agent", now)
	insertTeamTestMachineAndAgentForMember(t, ctx, db, fixture, secondAgentMemberID, "mac_expansion_second", "agt_expansion_second", "expansion-second-agent", now)
	insertTeamTestChannel(t, ctx, db, fixture.workspaceID, "chn_expansion", fixture.ownerMemberID, now)

	edges, err := store.ExpandTeamMentionsForScope(ctx, ExpandTeamMentionsParams{
		WorkspaceID:    fixture.workspaceID,
		ScopeID:        "chn_expansion",
		AuthorMemberID: fixture.ownerMemberID,
		TeamIDs:        []string{team.ID},
	})
	require.NoError(t, err)
	require.Empty(t, edges)

	insertTeamTestScopeMember(t, ctx, db, "chn_expansion", fixture.agentMemberID, "public", "", now)
	insertTeamTestScopeMember(t, ctx, db, "chn_expansion", secondAgentMemberID, "public", "", now)
	edges, err = store.ExpandTeamMentionsForScope(ctx, ExpandTeamMentionsParams{
		WorkspaceID:    fixture.workspaceID,
		ScopeID:        "chn_expansion",
		AuthorMemberID: fixture.ownerMemberID,
		TeamIDs:        []string{team.ID},
	})
	require.NoError(t, err)
	require.Len(t, edges, 2)
	require.Equal(t, "wtm_expansion_agent", edges[0].TeamMemberID)
	require.Equal(t, ActorKindAgent, edges[0].ActorKind)
	require.Equal(t, fixture.agentMemberID, edges[0].ActorID)
	require.Equal(t, MemberRoleLeader, edges[0].Role)
	require.Equal(t, "Builder", edges[0].RoleLabel)
	require.True(t, edges[0].WakeOnTeamMention)
	require.Equal(t, "wtm_expansion_second_agent", edges[1].TeamMemberID)
	require.Equal(t, secondAgentMemberID, edges[1].ActorID)
	require.Equal(t, MemberRoleMember, edges[1].Role)
	require.Equal(t, "Researcher", edges[1].RoleLabel)
	require.False(t, edges[1].WakeOnTeamMention)

	candidates, err := store.ListTeamMentionCandidatesForUser(ctx, fixture.ownerUserID, fixture.workspaceID, "expansion", 20, "chn_expansion")
	require.NoError(t, err)
	require.Len(t, candidates, 1)
	require.Equal(t, 2, candidates[0].ScopeAgentCount)
	require.Equal(t, 1, candidates[0].ReachableAgentCount)

	nonCreatorCandidates, err := store.ListTeamMentionCandidatesForUser(ctx, fixture.memberUserID, fixture.workspaceID, "expansion", 20, "chn_expansion")
	require.NoError(t, err)
	require.Empty(t, nonCreatorCandidates)

	nonCreatorEdgesByID, err := store.ExpandTeamMentionsForScope(ctx, ExpandTeamMentionsParams{
		WorkspaceID:    fixture.workspaceID,
		ScopeID:        "chn_expansion",
		AuthorMemberID: fixture.memberMemberID,
		TeamIDs:        []string{team.ID},
	})
	require.NoError(t, err)
	require.Empty(t, nonCreatorEdgesByID)

	nonCreatorEdgesByToken, err := store.ExpandTeamMentionsForScope(ctx, ExpandTeamMentionsParams{
		WorkspaceID:    fixture.workspaceID,
		ScopeID:        "chn_expansion",
		AuthorMemberID: fixture.memberMemberID,
		Tokens:         []string{"expansion"},
	})
	require.NoError(t, err)
	require.Empty(t, nonCreatorEdgesByToken)
}

func TestTeamTemplatesSaveAndInstantiateAgentAssignments(t *testing.T) {
	ctx := context.Background()
	db := startTeamTestPool(t, ctx)
	fixture := seedTeamStoreFixture(t, ctx, db)
	store := New(db)
	now := time.Date(2026, 6, 20, 10, 0, 0, 0, time.UTC)
	secondAgentMemberID := "mem_team_template_second_agent"
	insertTeamTestAgentMember(t, ctx, db, fixture, secondAgentMemberID, "Template Second Agent", now)

	sourceTeam, err := store.CreateTeam(ctx, CreateTeamParams{
		ID:                "team_template_source",
		WorkspaceID:       fixture.workspaceID,
		Handle:            "template-source",
		DisplayName:       "template-source",
		Description:       "Reusable team",
		Visibility:        VisibilityPrivate,
		CreatedByMemberID: fixture.ownerMemberID,
		InitialMembers: []AddTeamMemberParams{{
			ID:                "wtm_template_source_lead",
			ActorKind:         ActorKindAgent,
			ActorID:           fixture.agentMemberID,
			Role:              MemberRoleLeader,
			RoleLabel:         "Lead",
			RoleOverlayPrompt: "Coordinate from the template.",
		}},
		Now: now,
	})
	require.NoError(t, err)
	doNotWakeOnMention := false
	_, err = store.AddTeamMember(ctx, AddTeamMemberParams{
		ID:                "wtm_template_source_agent",
		WorkspaceID:       fixture.workspaceID,
		TeamID:            sourceTeam.ID,
		ActorKind:         ActorKindAgent,
		ActorID:           secondAgentMemberID,
		RoleLabel:         "Builder",
		RoleOverlayPrompt: "Build from the template.",
		WakeOnTeamMention: &doNotWakeOnMention,
		SortOrder:         30,
		AddedByMemberID:   fixture.ownerMemberID,
		Now:               now.Add(2 * time.Minute),
	})
	require.NoError(t, err)

	template, err := store.SaveTeamTemplateFromTeamForUser(ctx, fixture.ownerUserID, fixture.workspaceID, sourceTeam.ID, "feature-team", "Feature template", "")
	require.NoError(t, err)
	require.Equal(t, TemplateSourceCustom, template.Template.Source)
	require.Len(t, template.Members, 2)
	require.Equal(t, ActorKindAgent, template.Members[0].ActorKind)
	require.Equal(t, MemberRoleLeader, template.Members[0].Role)
	require.Equal(t, "Lead", template.Members[0].RoleLabel)
	require.True(t, template.Members[0].WakeOnTeamMention)
	require.Equal(t, "Builder", template.Members[1].RoleLabel)
	require.False(t, template.Members[1].WakeOnTeamMention)

	template, err = store.UpdateTeamTemplateForUser(ctx, fixture.ownerUserID, fixture.workspaceID, template.Template.ID, UpdateTeamTemplateParams{
		Name:        "feature-team",
		Description: "Feature template",
		Scope:       TemplateScopeWorkspace,
		Now:         now.Add(90 * time.Second),
	})
	require.NoError(t, err)

	require.NoError(t, store.DeleteTeam(ctx, fixture.workspaceID, sourceTeam.ID))

	created, err := store.InstantiateTeamTemplateForUser(ctx, fixture.ownerUserID, fixture.workspaceID, InstantiateTeamTemplateParams{
		TemplateID: template.Template.ID,
		Handle:     "from-template",
		Now:        now.Add(2 * time.Minute),
	})
	require.NoError(t, err)
	require.Equal(t, "from-template", created.Team.Handle)
	require.True(t, created.CanManage)

	members, err := store.ListTeamMembersForUser(ctx, fixture.ownerUserID, fixture.workspaceID, created.Team.ID)
	require.NoError(t, err)
	require.Len(t, members, 2)
	require.Equal(t, ActorKindAgent, members[0].Member.ActorKind)
	require.Equal(t, fixture.agentMemberID, members[0].Member.ActorID)
	require.Equal(t, MemberRoleLeader, members[0].Member.Role)
	require.Equal(t, "Lead", members[0].Member.RoleLabel)
	require.True(t, members[0].Member.WakeOnTeamMention)
	require.Equal(t, secondAgentMemberID, members[1].Member.ActorID)
	require.Equal(t, "Builder", members[1].Member.RoleLabel)
	require.False(t, members[1].Member.WakeOnTeamMention)

	_, err = store.InstantiateTeamTemplateForUser(ctx, fixture.ownerUserID, fixture.workspaceID, InstantiateTeamTemplateParams{
		TemplateID: template.Template.ID,
		Handle:     "from-template-skipped-slot",
		MemberBindings: []TeamTemplateMemberBinding{
			{
				TemplateMemberID: template.Members[0].ID,
				ActorID:          "",
			},
			{
				TemplateMemberID: template.Members[1].ID,
				ActorID:          "",
			},
		},
		Now: now.Add(3 * time.Minute),
	})
	require.ErrorIs(t, err, storeerr.ErrBadRequest)
}

func TestBuiltinTeamTemplatesAreAgentAssignmentsOnly(t *testing.T) {
	templates := builtinTeamTemplateViews("wsp_template_defaults", "", "")
	require.Len(t, templates, 6)
	names := make(map[string]bool, len(templates))
	wantLeaders := map[string]string{
		"builtin_bug_hunt_regression_fix": "btm_bug_hunt_root_cause",
		"builtin_code_review":             "btm_code_review_correctness",
		"builtin_decision_memo_crew":      "btm_decision_researcher",
		"builtin_feature_dev_squad":       "btm_feature_dev_squad_lead",
		"builtin_incident_triage_room":    "btm_incident_mitigation",
		"builtin_knowledge_builder":       "btm_knowledge_structure_editor",
	}
	for _, template := range templates {
		names[template.Template.Name] = true
		require.NotEmpty(t, template.Template.Description)
		wantLeader := wantLeaders[template.Template.ID]
		require.NotEmpty(t, wantLeader, "missing expected leader for %s", template.Template.ID)
		leaderCount := 0
		for _, member := range template.Members {
			require.Equal(t, ActorKindAgent, member.ActorKind)
			require.NotEmpty(t, member.RoleLabel)
			require.NotEmpty(t, member.RoleOverlayPrompt)
			if member.ID == wantLeader {
				require.Equal(t, MemberRoleLeader, member.Role)
				leaderCount++
			} else {
				require.Equal(t, MemberRoleMember, member.Role)
			}
		}
		require.Equal(t, 1, leaderCount)
	}
	require.True(t, names["Feature Dev Squad"], "expected Feature Dev Squad template to be present")
}

func TestBuiltinTeamTemplatesSwitchByLocale(t *testing.T) {
	englishViews := builtinTeamTemplateViews("wsp_template_defaults", "", "")
	require.NotEmpty(t, englishViews)

	englishByID, ok := builtinTeamTemplateByID("builtin_feature_dev_squad", "wsp_template_defaults", "")
	require.True(t, ok)
	require.Equal(t, "Feature Dev Squad", englishByID.Template.Name)
	require.NotEmpty(t, englishByID.Members)
	englishLeadLabel := englishByID.Members[0].RoleLabel
	require.NotEmpty(t, englishLeadLabel)

	// Look up under a non-English locale; Name and role labels localize.
	zhCN, ok := builtinTeamTemplateByID("builtin_feature_dev_squad", "wsp_template_defaults", "zh-CN")
	require.True(t, ok)
	require.NotEmpty(t, zhCN.Members)
	require.Equal(t, "功能开发小队", zhCN.Template.Name)
	require.NotEqual(t, englishLeadLabel, zhCN.Members[0].RoleLabel)

	// Unknown locale falls back to the default English cache.
	fallback, ok := builtinTeamTemplateByID("builtin_feature_dev_squad", "wsp_template_defaults", "klingon")
	require.True(t, ok)
	require.Equal(t, "Feature Dev Squad", fallback.Template.Name)
	require.Equal(t, englishLeadLabel, fallback.Members[0].RoleLabel)
}

func TestTeamManagementAPIPermissionsUseCreatorOnly(t *testing.T) {
	ctx := context.Background()
	db := startTeamTestPool(t, ctx)
	fixture := seedTeamStoreFixture(t, ctx, db)
	store := New(db)
	now := time.Date(2026, 6, 19, 10, 0, 0, 0, time.UTC)
	insertTeamTestMachineAndAgent(t, ctx, db, fixture, "mac_api_permissions_owner", "agt_api_permissions_owner", "Owner Agent", now)
	memberAgentMemberID := "mem_api_permissions_member_agent"
	ownerLeaderMemberID := "mem_api_permissions_owner_leader"
	insertTeamTestAgentMember(t, ctx, db, fixture, memberAgentMemberID, "Member Owned Agent", now)
	insertTeamTestMachineAndAgentForMemberCreatedBy(t, ctx, db, fixture, memberAgentMemberID, fixture.memberUserID, "mac_api_permissions_member", "agt_api_permissions_member", "Member Owned Agent", now)
	insertTeamTestAgentMember(t, ctx, db, fixture, ownerLeaderMemberID, "Owner Leader Agent", now)
	insertTeamTestMachineAndAgentForMember(t, ctx, db, fixture, ownerLeaderMemberID, "mac_api_permissions_leader", "agt_api_permissions_leader", "Owner Leader Agent", now)

	memberCreated, err := store.CreateTeamForUser(ctx, fixture.memberUserID, fixture.workspaceID, CreateTeamParams{
		Handle:         "member-team",
		DisplayName:    "Member Team",
		Visibility:     VisibilityWorkspace,
		InitialMembers: initialTeamLeader(memberAgentMemberID),
	})
	require.NoError(t, err)
	require.Equal(t, VisibilityPrivate, memberCreated.Team.Visibility)
	require.True(t, memberCreated.CanManage)
	require.True(t, memberCreated.CanDelete)
	ownerTeams, err := store.ListTeamsForUser(ctx, fixture.ownerUserID, fixture.workspaceID, ListTeamsFilter{Status: "all"})
	require.NoError(t, err)
	require.NotContains(t, teamViewIDs(ownerTeams), memberCreated.Team.ID)
	ownerCandidates, err := store.ListTeamMentionCandidatesForUser(ctx, fixture.ownerUserID, fixture.workspaceID, "member", 20, "")
	require.NoError(t, err)
	require.NotContains(t, teamCandidateIDs(ownerCandidates), memberCreated.Team.ID)
	_, err = store.GetTeamForUser(ctx, fixture.ownerUserID, fixture.workspaceID, memberCreated.Team.ID)
	require.ErrorIs(t, err, storeerr.ErrNotFound)
	err = store.DeleteTeamForUser(ctx, fixture.ownerUserID, fixture.workspaceID, memberCreated.Team.ID)
	require.ErrorIs(t, err, storeerr.ErrForbidden)
	updatedDescription := "workspace owners still cannot manage this team"
	_, err = store.UpdateTeamForUser(ctx, fixture.ownerUserID, fixture.workspaceID, memberCreated.Team.ID, UpdateTeamParams{
		Description: &updatedDescription,
	})
	require.ErrorIs(t, err, storeerr.ErrForbidden)
	require.NoError(t, store.DeleteTeamForUser(ctx, fixture.memberUserID, fixture.workspaceID, memberCreated.Team.ID))

	created, err := store.CreateTeamForUser(ctx, fixture.ownerUserID, fixture.workspaceID, CreateTeamParams{
		ID:             "team_api_permissions",
		Handle:         "api-permissions",
		DisplayName:    "API Permissions",
		Visibility:     VisibilityPrivate,
		InitialMembers: initialTeamLeader(ownerLeaderMemberID),
	})
	require.NoError(t, err)
	require.True(t, created.CanManage)
	require.True(t, created.CanDelete)
	require.Equal(t, VisibilityPrivate, created.Team.Visibility)

	_, err = store.AddTeamMemberForUser(ctx, fixture.memberUserID, fixture.workspaceID, created.Team.ID, AddTeamMemberParams{
		ActorKind: ActorKindAgent,
		ActorID:   fixture.agentMemberID,
	})
	require.ErrorIs(t, err, storeerr.ErrForbidden)

	agentMember, err := store.AddTeamMemberForUser(ctx, fixture.ownerUserID, fixture.workspaceID, created.Team.ID, AddTeamMemberParams{
		ActorKind:         ActorKindAgent,
		ActorID:           fixture.agentMemberID,
		RoleLabel:         "Builder",
		RoleOverlayPrompt: "Build the task.",
		SortOrder:         5,
	})
	require.NoError(t, err)
	require.Equal(t, "Builder", agentMember.Member.RoleLabel)
	require.Equal(t, "agt_api_permissions_owner", agentMember.ActorProfile.PrincipalID)
	require.Equal(t, fixture.agentMemberID, agentMember.ActorProfile.MemberID)
	require.Equal(t, "Owner Agent", agentMember.ActorProfile.TargetID)
	require.Equal(t, "Owner Agent", agentMember.ActorProfile.DisplayName)
	require.Equal(t, "/api/v1/workspaces/wsp_team_store/agents/agt_api_permissions_owner/avatar", agentMember.ActorProfile.AvatarURL)

	_, err = store.AddTeamMemberForUser(ctx, fixture.ownerUserID, fixture.workspaceID, created.Team.ID, AddTeamMemberParams{
		ActorKind: ActorKindHuman,
		ActorID:   fixture.memberMemberID,
	})
	require.ErrorIs(t, err, storeerr.ErrBadRequest)

	err = store.RemoveTeamMemberForUser(ctx, fixture.memberUserID, fixture.workspaceID, created.Team.ID, agentMember.Member.ID)
	require.ErrorIs(t, err, storeerr.ErrForbidden)
	require.NoError(t, store.RemoveTeamMemberForUser(ctx, fixture.ownerUserID, fixture.workspaceID, created.Team.ID, agentMember.Member.ID))

	require.NoError(t, store.DeleteTeamForUser(ctx, fixture.ownerUserID, fixture.workspaceID, created.Team.ID))
	_, err = store.GetTeamForUser(ctx, fixture.ownerUserID, fixture.workspaceID, created.Team.ID)
	require.ErrorIs(t, err, storeerr.ErrNotFound)
}

func seedTeamStoreFixture(t *testing.T, ctx context.Context, db coredb.DB) teamStoreFixture {
	t.Helper()
	fixture := teamStoreFixture{
		workspaceID:    "wsp_team_store",
		ownerUserID:    "usr_team_owner",
		ownerMemberID:  "mem_team_owner",
		memberUserID:   "usr_team_member",
		memberMemberID: "mem_team_member",
		agentMemberID:  "mem_team_agent",
	}
	mustExecTeamTest(t, ctx, db, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES
  ($1, 'owner@example.com', 'Owner', 'Owner', '', TRUE, '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
  ($2, 'member@example.com', 'Member', 'Member', '', FALSE, '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`, fixture.ownerUserID, fixture.memberUserID)
	mustExecTeamTest(t, ctx, db, `
INSERT INTO organizations (id, slug, name, created_by_user_id, created_at, updated_at)
VALUES ('org_team_store', 'team-store-org', 'Team Store Org', $1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`, fixture.ownerUserID)
	mustExecTeamTest(t, ctx, db, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, 'org_team_store', 'team-store-workspace', 'Team Store Workspace', 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`, fixture.workspaceID)
	mustExecTeamTest(t, ctx, db, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES
  ($1, $4, 'human', 'Owner', $5, 'owner', 'active', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
  ($2, $4, 'human', 'Member', $6, 'member', 'active', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
  ($3, $4, 'agent', 'Agent', NULL, NULL, 'active', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`, fixture.ownerMemberID, fixture.memberMemberID, fixture.agentMemberID, fixture.workspaceID, fixture.ownerUserID, fixture.memberUserID)
	return fixture
}

func insertTeamTestMachineAndAgent(t *testing.T, ctx context.Context, db coredb.QueryExecer, fixture teamStoreFixture, machineID string, agentID string, agentName string, now time.Time) {
	t.Helper()
	insertTeamTestMachineAndAgentForMember(t, ctx, db, fixture, fixture.agentMemberID, machineID, agentID, agentName, now)
}

func insertTeamTestMachineAndAgentForMember(t *testing.T, ctx context.Context, db coredb.QueryExecer, fixture teamStoreFixture, agentMemberID string, machineID string, agentID string, agentName string, now time.Time) {
	t.Helper()
	insertTeamTestMachineAndAgentForMemberCreatedBy(t, ctx, db, fixture, agentMemberID, fixture.ownerUserID, machineID, agentID, agentName, now)
}

func insertTeamTestMachineAndAgentForMemberCreatedBy(t *testing.T, ctx context.Context, db coredb.QueryExecer, fixture teamStoreFixture, agentMemberID string, createdByUserID string, machineID string, agentID string, agentName string, now time.Time) {
	t.Helper()
	mustExecTeamTest(t, ctx, db, `
INSERT INTO machines (id, workspace_id, created_by_user_id, name, api_key, created_at, updated_at)
VALUES ($1, $2, $3, $1, $4, $5, $5)
`, machineID, fixture.workspaceID, createdByUserID, "key-"+machineID, now)
	mustExecTeamTest(t, ctx, db, `
INSERT INTO agents (id, workspace_id, machine_id, member_id, name, avatar_url, runtime, model, description, auth_token_hash, activity_detail, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, 'codex', 'gpt-5', '', NULL, '', $7, $7)
`, agentID, fixture.workspaceID, machineID, agentMemberID, agentName, "/api/v1/workspaces/"+fixture.workspaceID+"/agents/"+agentID+"/avatar", now)
}

func insertTeamTestAgentMember(t *testing.T, ctx context.Context, db coredb.QueryExecer, fixture teamStoreFixture, memberID string, displayName string, now time.Time) {
	t.Helper()
	mustExecTeamTest(t, ctx, db, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES ($1, $2, 'agent', $3, NULL, NULL, 'active', $4, $4)
`, memberID, fixture.workspaceID, displayName, now)
}

func insertTeamTestChannel(t *testing.T, ctx context.Context, db coredb.QueryExecer, workspaceID string, scopeID string, ownerMemberID string, now time.Time) {
	t.Helper()
	mustExecTeamTest(t, ctx, db, `
INSERT INTO scopes (
    id, workspace_id, type, identity_key, latest_seq, latest_global_seq,
    latest_message_at, archived_until_seq, status, created_by_member_id,
    created_at, updated_at
)
VALUES ($1, $2, 'channel', $3, 0, 0, NULL, 0, 'active', $4, $5, $5)
`, scopeID, workspaceID, "channel:"+scopeID, ownerMemberID, now)
	mustExecTeamTest(t, ctx, db, `
INSERT INTO scope_channel_profiles (
    scope_id, workspace_id, slug, display_name, description, visibility,
    owner_member_id, is_all_channel, created_at, updated_at
)
VALUES ($1, $2, $1, $1, '', 'private', $3, FALSE, $4, $4)
`, scopeID, workspaceID, ownerMemberID, now)
	insertTeamTestScopeMember(t, ctx, db, scopeID, ownerMemberID, "public", "", now)
}

func insertTeamTestScopeMember(t *testing.T, ctx context.Context, db coredb.QueryExecer, scopeID string, memberID string, visibility string, ownerMemberID string, now time.Time) {
	t.Helper()
	if visibility == "private" && ownerMemberID == "" {
		ownerMemberID = memberID
	}
	mustExecTeamTest(t, ctx, db, `
INSERT INTO scope_memberships (
    scope_id, member_id, visibility, owner_member_id, last_read_seq,
    last_read_global_seq, last_activity_global_seq, owner_context_dismissed_through_global_seq,
    effective_from_seq, effective_to_seq, created_at, updated_at, removed_at, archived_at
)
VALUES ($1, $2, $3, NULLIF($4, ''), 0, 0, 0, 0, 0, NULL, $5, $5, NULL, NULL)
`, scopeID, memberID, visibility, ownerMemberID, now)
}

func mustExecTeamTest(t *testing.T, ctx context.Context, db coredb.QueryExecer, query string, args ...any) {
	t.Helper()
	_, err := db.Exec(ctx, query, args...)
	require.NoError(t, err)
}

func teamViewIDs(views []TeamView) []string {
	ids := make([]string, 0, len(views))
	for _, view := range views {
		ids = append(ids, view.Team.ID)
	}
	return ids
}

func teamCandidateIDs(candidates []TeamMentionCandidate) []string {
	ids := make([]string, 0, len(candidates))
	for _, candidate := range candidates {
		ids = append(ids, candidate.Team.ID)
	}
	return ids
}

func TestInstantiateBuiltinIncidentTemplateSkipsUnboundOptionalStatusSlot(t *testing.T) {
	ctx := context.Background()
	db := startTeamTestPool(t, ctx)
	fixture := seedTeamStoreFixture(t, ctx, db)
	store := New(db)
	now := time.Date(2026, 6, 27, 6, 0, 0, 0, time.UTC)

	logsMemberID := fixture.agentMemberID
	infraMemberID := "mem_team_incident_infra"
	mitigationMemberID := "mem_team_incident_mitigation"
	insertTeamTestMachineAndAgentForMember(t, ctx, db, fixture, logsMemberID, "mac_incident_logs", "agt_incident_logs", "incident-logs", now)
	insertTeamTestAgentMember(t, ctx, db, fixture, infraMemberID, "Incident Infra", now)
	insertTeamTestMachineAndAgentForMember(t, ctx, db, fixture, infraMemberID, "mac_incident_infra", "agt_incident_infra", "incident-infra", now)
	insertTeamTestAgentMember(t, ctx, db, fixture, mitigationMemberID, "Incident Mitigation", now)
	insertTeamTestMachineAndAgentForMember(t, ctx, db, fixture, mitigationMemberID, "mac_incident_mitigation", "agt_incident_mitigation", "incident-mitigation", now)

	result, err := store.InstantiateTeamTemplate(ctx, fixture.ownerUserID, fixture.workspaceID, InstantiateTeamTemplateParams{
		TemplateID: "builtin_incident_triage_room",
		Handle:     "incident-triage-room-runtime",
		MemberBindings: []TeamTemplateMemberBinding{
			{TemplateMemberID: "btm_incident_logs", ActorID: logsMemberID},
			{TemplateMemberID: "btm_incident_infra", ActorID: infraMemberID},
			{TemplateMemberID: "btm_incident_mitigation", ActorID: mitigationMemberID},
		},
		Now: now.Add(time.Minute),
	})
	require.NoError(t, err)
	require.Equal(t, "incident-triage-room-runtime", result.Team.Team.Handle)

	members, err := store.ListActiveTeamMembers(ctx, fixture.workspaceID, result.Team.Team.ID)
	require.NoError(t, err)
	require.Len(t, members, 3)
}

func TestInstantiateBuiltinFeatureDevSquadUsesRequestedLocaleTemplateAssets(t *testing.T) {
	ctx := context.Background()
	db := startTeamTestPool(t, ctx)
	fixture := seedTeamStoreFixture(t, ctx, db)
	store := New(db)
	now := time.Date(2026, 6, 27, 7, 0, 0, 0, time.UTC)

	englishTemplate, err := store.GetTeamTemplateForUser(ctx, fixture.ownerUserID, fixture.workspaceID, "builtin_feature_dev_squad", "")
	require.NoError(t, err)

	zhCNTemplate, err := store.GetTeamTemplateForUser(ctx, fixture.ownerUserID, fixture.workspaceID, "builtin_feature_dev_squad", "zh-CN")
	require.NoError(t, err)
	require.Len(t, zhCNTemplate.Members, 4)
	require.Equal(t, len(englishTemplate.Members), len(zhCNTemplate.Members))
	require.NotEqual(t, englishTemplate.Template.Name, zhCNTemplate.Template.Name)
	require.NotEqual(t, englishTemplate.Members[0].RoleLabel, zhCNTemplate.Members[0].RoleLabel)

	frontendMemberID := "mem_team_feature_dev_frontend_zh"
	backendMemberID := "mem_team_feature_dev_backend_zh"
	reviewerMemberID := "mem_team_feature_dev_reviewer_zh"
	insertTeamTestAgentMember(t, ctx, db, fixture, frontendMemberID, "Feature Frontend Agent", now)
	insertTeamTestAgentMember(t, ctx, db, fixture, backendMemberID, "Feature Backend Agent", now)
	insertTeamTestAgentMember(t, ctx, db, fixture, reviewerMemberID, "Feature Reviewer Agent", now)

	bindings := []TeamTemplateMemberBinding{
		{TemplateMemberID: zhCNTemplate.Members[0].ID, ActorID: fixture.agentMemberID},
		{TemplateMemberID: zhCNTemplate.Members[1].ID, ActorID: frontendMemberID},
		{TemplateMemberID: zhCNTemplate.Members[2].ID, ActorID: backendMemberID},
		{TemplateMemberID: zhCNTemplate.Members[3].ID, ActorID: reviewerMemberID},
	}
	result, err := store.InstantiateTeamTemplate(ctx, fixture.ownerUserID, fixture.workspaceID, InstantiateTeamTemplateParams{
		TemplateID:     zhCNTemplate.Template.ID,
		Handle:         "feature-dev-squad-zh-runtime",
		MemberBindings: bindings,
		Locale:         "zh-CN",
		Now:            now.Add(time.Minute),
	})
	require.NoError(t, err)
	require.Equal(t, zhCNTemplate.Template.Description, result.Team.Team.Description)

	members, err := store.ListActiveTeamMembers(ctx, fixture.workspaceID, result.Team.Team.ID)
	require.NoError(t, err)
	require.Len(t, members, len(bindings))

	membersByActorID := make(map[string]model.WorkspaceTeamMember, len(members))
	for _, member := range members {
		membersByActorID[member.ActorID] = member
	}
	for index, binding := range bindings {
		got, ok := membersByActorID[binding.ActorID]
		require.True(t, ok, "missing instantiated team member for actor %s", binding.ActorID)
		want := zhCNTemplate.Members[index]
		require.Equal(t, want.Role, got.Role, binding.ActorID)
		require.Equal(t, want.RoleLabel, got.RoleLabel, binding.ActorID)
		require.Equal(t, want.RoleOverlayPrompt, got.RoleOverlayPrompt, binding.ActorID)
		require.Equal(t, want.WakeOnTeamMention, got.WakeOnTeamMention, binding.ActorID)
	}
}
