package access

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/store/testfixture"
)

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

	tests := []struct {
		name    string
		role    domain.WorkspaceRole
		action  HumanAction
		wantErr error
	}{
		{
			name:   "owner can manage workspace members",
			role:   domain.WorkspaceRoleOwner,
			action: HumanAction{Resource: ResourceWorkspace, Action: ActionManageMembers},
		},
		{
			name:   "owner can manage member roles",
			role:   domain.WorkspaceRoleOwner,
			action: HumanAction{Resource: ResourceWorkspace, Action: ActionManageMemberRoles},
		},
		{
			name:   "admin can manage runtime",
			role:   domain.WorkspaceRoleAdmin,
			action: HumanAction{Resource: ResourceWorkspace, Action: ActionManageRuntime},
		},
		{
			name:   "admin can install services",
			role:   domain.WorkspaceRoleAdmin,
			action: HumanAction{Resource: ResourceWorkspace, Action: ActionInstallService},
		},
		{
			name:    "admin cannot manage member roles",
			role:    domain.WorkspaceRoleAdmin,
			action:  HumanAction{Resource: ResourceWorkspace, Action: ActionManageMemberRoles},
			wantErr: ErrForbidden,
		},
		{
			name:   "member can access workspace",
			role:   domain.WorkspaceRoleMember,
			action: HumanAction{Resource: ResourceWorkspace, Action: ActionAccess},
		},
		{
			name:    "member cannot manage runtime",
			role:    domain.WorkspaceRoleMember,
			action:  HumanAction{Resource: ResourceWorkspace, Action: ActionManageRuntime},
			wantErr: ErrForbidden,
		},
		{
			name:    "member cannot install services",
			role:    domain.WorkspaceRoleMember,
			action:  HumanAction{Resource: ResourceWorkspace, Action: ActionInstallService},
			wantErr: ErrForbidden,
		},
		{
			name:   "admin can manage machine",
			role:   domain.WorkspaceRoleAdmin,
			action: HumanAction{Resource: ResourceMachine, Action: ActionManage},
		},
		{
			name:   "admin can approve service installations",
			role:   domain.WorkspaceRoleAdmin,
			action: HumanAction{Resource: ResourceServiceInstallation, Action: ActionApprove},
		},
		{
			name:    "member cannot approve service installations",
			role:    domain.WorkspaceRoleMember,
			action:  HumanAction{Resource: ResourceServiceInstallation, Action: ActionApprove},
			wantErr: ErrForbidden,
		},
		{
			name:    "member cannot operate agent",
			role:    domain.WorkspaceRoleMember,
			action:  HumanAction{Resource: ResourceAgent, Action: ActionOperate},
			wantErr: ErrForbidden,
		},
		{
			name:    "admin cannot use agent self act as action",
			role:    domain.WorkspaceRoleAdmin,
			action:  HumanAction{Resource: ResourceAgent, Action: ActionActAsMember},
			wantErr: ErrInvalidAuthorizationRequest,
		},
		{
			name:    "admin cannot use service invoke without installation grant path",
			role:    domain.WorkspaceRoleAdmin,
			action:  HumanAction{Resource: ResourceServiceInstallation, Action: ActionInvoke},
			wantErr: ErrInvalidAuthorizationRequest,
		},
		{
			name:    "unsupported action is rejected",
			role:    domain.WorkspaceRoleOwner,
			action:  HumanAction{Resource: ResourceWorkspace, Action: ActionManage},
			wantErr: ErrInvalidAuthorizationRequest,
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			err := AuthorizeWorkspaceRole(tt.role, tt.action)
			if tt.wantErr == nil {
				require.NoError(t, err)
				return
			}
			require.ErrorIs(t, err, tt.wantErr)
		})
	}
}

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

	tests := []struct {
		name          string
		channelAccess HumanChannelAccess
		action        HumanAction
		wantErr       error
	}{
		{
			name: "joined member can read channel",
			channelAccess: HumanChannelAccess{
				WorkspaceMember: HumanWorkspaceMember{WorkspaceRole: "member"},
				Channel:         Channel{Type: "channel"},
				Joined:          true,
				CanInvite:       true,
				IsOwner:         false,
			},
			action: HumanAction{Resource: ResourceChannel, Action: ActionRead},
		},
		{
			name: "joined member can post channel",
			channelAccess: HumanChannelAccess{
				WorkspaceMember: HumanWorkspaceMember{WorkspaceRole: "member"},
				Channel:         Channel{Type: "channel"},
				Joined:          true,
				CanInvite:       true,
				IsOwner:         false,
			},
			action: HumanAction{Resource: ResourceChannel, Action: ActionPost},
		},
		{
			name: "joined member with invite fact can invite members",
			channelAccess: HumanChannelAccess{
				WorkspaceMember: HumanWorkspaceMember{WorkspaceRole: "member"},
				Channel:         Channel{Type: "channel"},
				Joined:          true,
				CanInvite:       true,
				IsOwner:         false,
			},
			action: HumanAction{Resource: ResourceChannel, Action: ActionInviteMembers},
		},
		{
			name: "joined member without invite fact cannot invite members",
			channelAccess: HumanChannelAccess{
				WorkspaceMember: HumanWorkspaceMember{WorkspaceRole: "member"},
				Channel:         Channel{Type: "channel"},
				Joined:          true,
				CanInvite:       false,
				IsOwner:         false,
			},
			action:  HumanAction{Resource: ResourceChannel, Action: ActionInviteMembers},
			wantErr: ErrForbidden,
		},
		{
			name: "joined non owner cannot owner manage channel",
			channelAccess: HumanChannelAccess{
				WorkspaceMember: HumanWorkspaceMember{WorkspaceRole: "member"},
				Channel:         Channel{Type: "channel"},
				Joined:          true,
				CanInvite:       true,
				IsOwner:         false,
			},
			action:  HumanAction{Resource: ResourceChannel, Action: ActionManageMembers},
			wantErr: ErrForbidden,
		},
		{
			name: "joined owner can owner manage channel",
			channelAccess: HumanChannelAccess{
				WorkspaceMember: HumanWorkspaceMember{WorkspaceRole: "member"},
				Channel:         Channel{Type: "channel"},
				Joined:          true,
				CanInvite:       true,
				IsOwner:         true,
			},
			action: HumanAction{Resource: ResourceChannel, Action: ActionManageMembers},
		},
		{
			name: "workspace admin not joined cannot owner manage private channel",
			channelAccess: HumanChannelAccess{
				WorkspaceMember: HumanWorkspaceMember{WorkspaceRole: "admin"},
				Channel:         Channel{Type: "channel"},
				Joined:          false,
				CanInvite:       false,
				IsOwner:         false,
			},
			action:  HumanAction{Resource: ResourceChannel, Action: ActionManageMembers},
			wantErr: ErrForbidden,
		},
		{
			name: "invalid channel action is rejected",
			channelAccess: HumanChannelAccess{
				WorkspaceMember: HumanWorkspaceMember{WorkspaceRole: "owner"},
				Channel:         Channel{Type: "channel"},
				Joined:          true,
				CanInvite:       true,
				IsOwner:         true,
			},
			action:  HumanAction{Resource: ResourceChannel, Action: ActionAccess},
			wantErr: ErrInvalidAuthorizationRequest,
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			err := AuthorizeHumanChannelAction(tt.channelAccess, tt.action)
			if tt.wantErr == nil {
				require.NoError(t, err)
				return
			}
			require.ErrorIs(t, err, tt.wantErr)
		})
	}
}

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

	tests := []struct {
		name     string
		dmAccess HumanDMAccess
		action   HumanAction
		wantErr  error
	}{
		{
			name:     "participant can read dm",
			dmAccess: HumanDMAccess{Participant: true},
			action:   HumanAction{Resource: ResourceDM, Action: ActionRead},
		},
		{
			name:     "participant can post dm",
			dmAccess: HumanDMAccess{Participant: true},
			action:   HumanAction{Resource: ResourceDM, Action: ActionPost},
		},
		{
			name:     "non participant cannot read dm",
			dmAccess: HumanDMAccess{Participant: false},
			action:   HumanAction{Resource: ResourceDM, Action: ActionRead},
			wantErr:  ErrForbidden,
		},
		{
			name:     "invalid dm action is rejected",
			dmAccess: HumanDMAccess{Participant: true},
			action:   HumanAction{Resource: ResourceDM, Action: ActionManageMembers},
			wantErr:  ErrInvalidAuthorizationRequest,
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			err := AuthorizeHumanDMAction(tt.dmAccess, tt.action)
			if tt.wantErr == nil {
				require.NoError(t, err)
				return
			}
			require.ErrorIs(t, err, tt.wantErr)
		})
	}
}

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

	tests := []struct {
		name         string
		threadAccess HumanThreadAccess
		action       HumanAction
		wantErr      error
	}{
		{
			name: "joined parent channel member can read thread",
			threadAccess: HumanThreadAccess{
				ParentThreadType:    domain.ThreadTypeChannel,
				ParentChannelJoined: true,
			},
			action: HumanAction{Resource: ResourceThread, Action: ActionRead},
		},
		{
			name: "dm participant can post thread reply",
			threadAccess: HumanThreadAccess{
				ParentThreadType:    domain.ThreadTypeDM,
				ParentDMParticipant: true,
			},
			action: HumanAction{Resource: ResourceThread, Action: ActionPost},
		},
		{
			name: "non joined channel member cannot read thread",
			threadAccess: HumanThreadAccess{
				ParentThreadType:    domain.ThreadTypeChannel,
				ParentChannelJoined: false,
			},
			action:  HumanAction{Resource: ResourceThread, Action: ActionRead},
			wantErr: ErrForbidden,
		},
		{
			name: "non participant cannot post dm thread",
			threadAccess: HumanThreadAccess{
				ParentThreadType:    domain.ThreadTypeDM,
				ParentDMParticipant: false,
			},
			action:  HumanAction{Resource: ResourceThread, Action: ActionPost},
			wantErr: ErrForbidden,
		},
		{
			name: "invalid thread action is rejected",
			threadAccess: HumanThreadAccess{
				ParentThreadType:    domain.ThreadTypeChannel,
				ParentChannelJoined: true,
			},
			action:  HumanAction{Resource: ResourceThread, Action: ActionManageMembers},
			wantErr: ErrInvalidAuthorizationRequest,
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			err := AuthorizeHumanThreadAction(tt.threadAccess, tt.action)
			if tt.wantErr == nil {
				require.NoError(t, err)
				return
			}
			require.ErrorIs(t, err, tt.wantErr)
		})
	}
}

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

	ctx := context.Background()
	pool := startAccessTestPool(t, ctx)

	now := time.Date(2026, 3, 20, 9, 0, 0, 0, time.UTC)
	workspaceID := "wsp_access_authorizer"
	userID := "usr_access_authorizer"
	memberID := "mem_access_authorizer"

	mustExec(t, ctx, pool, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES ($1, 'authorizer@example.com', 'authorizer', 'Authorizer User', '', FALSE, 'hash', $2, $2, $2)
`, userID, now)
	mustExec(t, ctx, pool, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'access-authorizer', 'access-authorizer', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	mustExec(t, ctx, pool, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, created_at, updated_at)
VALUES ($1, $2, 'human', 'Authorizer User', $3, 'member', $4, $4)
`, memberID, workspaceID, userID, now)

	membership, err := LoadAuthorizedHumanWorkspaceMember(ctx, pool, userID, workspaceID, HumanAction{
		Resource: ResourceWorkspace,
		Action:   ActionAccess,
	})
	require.NoError(t, err)
	require.Equal(t, memberID, membership.Member.ID)

	_, err = LoadAuthorizedHumanWorkspaceMember(ctx, pool, userID, workspaceID, HumanAction{
		Resource: ResourceWorkspace,
		Action:   ActionManageMembers,
	})
	require.ErrorIs(t, err, ErrForbidden)
}

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

	ctx := context.Background()
	pool := startAccessTestPool(t, ctx)

	now := time.Date(2026, 3, 20, 10, 0, 0, 0, time.UTC)
	workspaceID := "wsp_access_channel"
	channelID := "chn_access_channel"
	memberUserID := "usr_access_channel_member"
	memberMemberID := "mem_access_channel_member"
	guestUserID := "usr_access_channel_guest"
	guestMemberID := "mem_access_channel_guest"
	adminUserID := "usr_access_channel_admin"
	adminMemberID := "mem_access_channel_admin"

	mustExec(t, ctx, pool, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'access-channel', 'access-channel', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	mustExec(t, ctx, pool, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES
($1, 'member@example.com', 'member', 'Joined Member', '', FALSE, 'hash', $3, $3, $3),
($2, 'guest@example.com', 'guest', 'Joined Guest', '', FALSE, 'hash', $3, $3, $3),
($4, 'admin@example.com', 'admin', 'Workspace Admin', '', FALSE, 'hash', $3, $3, $3)
`, memberUserID, guestUserID, now, adminUserID)
	mustExec(t, ctx, pool, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, created_at, updated_at)
VALUES
($1, $3, 'human', 'Joined Member', $4, 'member', $5, $5),
($2, $3, 'human', 'Joined Guest', $6, 'member', $5, $5),
($7, $3, 'human', 'Workspace Admin', $8, 'admin', $5, $5)
`, memberMemberID, guestMemberID, workspaceID, memberUserID, now, guestUserID, adminMemberID, adminUserID)
	mustSeedChannelScope(t, ctx, pool, workspaceID, channelID, "restricted", "restricted", "restricted", "public", memberMemberID, false, now)
	mustSeedScopeMembership(t, ctx, pool, channelID, memberMemberID, now)
	mustSeedScopeMembership(t, ctx, pool, channelID, guestMemberID, now)

	memberAccess, err := LoadAuthorizedHumanChannelAccess(ctx, pool, memberUserID, workspaceID, channelID, HumanAction{
		Resource: ResourceChannel,
		Action:   ActionRead,
	})
	require.NoError(t, err)
	require.True(t, memberAccess.Joined)
	require.True(t, memberAccess.CanInvite)
	require.True(t, memberAccess.IsOwner)

	memberInviteAccess, err := LoadAuthorizedHumanChannelAccess(ctx, pool, memberUserID, workspaceID, channelID, HumanAction{
		Resource: ResourceChannel,
		Action:   ActionInviteMembers,
	})
	require.NoError(t, err)
	require.True(t, memberInviteAccess.CanInvite)

	_, err = LoadAuthorizedHumanChannelAccess(ctx, pool, guestUserID, workspaceID, channelID, HumanAction{
		Resource: ResourceChannel,
		Action:   ActionManageMembers,
	})
	require.ErrorIs(t, err, ErrForbidden)

	_, err = LoadAuthorizedHumanChannelAccess(ctx, pool, adminUserID, workspaceID, channelID, HumanAction{
		Resource: ResourceChannel,
		Action:   ActionManageMembers,
	})
	require.ErrorIs(t, err, ErrForbidden)

	_, err = LoadAuthorizedHumanChannelAccess(ctx, pool, adminUserID, workspaceID, channelID, HumanAction{
		Resource: ResourceChannel,
		Action:   ActionRead,
	})
	require.ErrorIs(t, err, ErrForbidden)
}

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

	ctx := context.Background()
	pool := startAccessTestPool(t, ctx)

	now := time.Date(2026, 3, 20, 11, 0, 0, 0, time.UTC)
	workspaceID := "wsp_access_dm"
	dmThreadID := "dmt_access_dm"
	memberUserID := "usr_access_dm_member"
	memberMemberID := "mem_access_dm_member"
	guestUserID := "usr_access_dm_guest"
	guestMemberID := "mem_access_dm_guest"
	peerMemberID := "mem_access_dm_peer"

	mustExec(t, ctx, pool, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'access-dm', 'access-dm', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	mustExec(t, ctx, pool, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES
($1, 'member@example.com', 'member', 'DM Member', '', FALSE, 'hash', $4, $4, $4),
($2, 'guest@example.com', 'guest', 'DM Guest', '', FALSE, 'hash', $4, $4, $4),
($3, 'peer@example.com', 'peer', 'DM Peer', '', FALSE, 'hash', $4, $4, $4)
`, memberUserID, guestUserID, "usr_access_dm_peer", now)
	mustExec(t, ctx, pool, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, created_at, updated_at)
VALUES
($1, $4, 'human', 'DM Member', $5, 'member', $6, $6),
($2, $4, 'human', 'DM Guest', $7, 'member', $6, $6),
($3, $4, 'human', 'DM Peer', $8, 'member', $6, $6)
`, memberMemberID, guestMemberID, peerMemberID, workspaceID, memberUserID, now, guestUserID, "usr_access_dm_peer")
	mustSeedDMScope(t, ctx, pool, workspaceID, dmThreadID, now)
	mustSeedScopeMembership(t, ctx, pool, dmThreadID, memberMemberID, now)
	mustSeedScopeMembership(t, ctx, pool, dmThreadID, peerMemberID, now)

	access, err := LoadAuthorizedHumanDMAccess(ctx, pool, memberUserID, workspaceID, dmThreadID, HumanAction{
		Resource: ResourceDM,
		Action:   ActionRead,
	})
	require.NoError(t, err)
	require.True(t, access.Participant)
	require.Equal(t, peerMemberID, access.PeerMemberID)
	require.Equal(t, dmThreadID, access.ThreadID)

	_, err = LoadAuthorizedHumanDMAccess(ctx, pool, guestUserID, workspaceID, dmThreadID, HumanAction{
		Resource: ResourceDM,
		Action:   ActionRead,
	})
	require.ErrorIs(t, err, ErrForbidden)
}

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

	ctx := context.Background()
	pool := startAccessTestPool(t, ctx)

	now := time.Date(2026, 3, 20, 12, 0, 0, 0, time.UTC)
	workspaceID := "wsp_access_thread"
	parentChannelID := "chn_access_thread_parent"
	replyChannelID := "chn_access_thread_reply"
	dmThreadID := "dmt_access_thread"
	dmReplyChannelID := "chn_access_thread_dm_reply"
	memberUserID := "usr_access_thread_member"
	memberMemberID := "mem_access_thread_member"
	guestUserID := "usr_access_thread_guest"
	guestMemberID := "mem_access_thread_guest"
	peerMemberID := "mem_access_thread_peer"

	mustExec(t, ctx, pool, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'access-thread', 'access-thread', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	mustExec(t, ctx, pool, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES
($1, 'member@example.com', 'member', 'Thread Member', '', FALSE, 'hash', $4, $4, $4),
($2, 'guest@example.com', 'guest', 'Thread Guest', '', FALSE, 'hash', $4, $4, $4),
($3, 'peer@example.com', 'peer', 'Thread Peer', '', FALSE, 'hash', $4, $4, $4)
`, memberUserID, guestUserID, "usr_access_thread_peer", now)
	mustExec(t, ctx, pool, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, created_at, updated_at)
VALUES
($1, $4, 'human', 'Thread Member', $5, 'member', $6, $6),
($2, $4, 'human', 'Thread Guest', $7, 'member', $6, $6),
($3, $4, 'human', 'Thread Peer', $8, 'member', $6, $6)
`, memberMemberID, guestMemberID, peerMemberID, workspaceID, memberUserID, now, guestUserID, "usr_access_thread_peer")
	mustSeedChannelScope(t, ctx, pool, workspaceID, parentChannelID, "restricted", "restricted", "restricted", "public", "", false, now)
	mustSeedScopeMembership(t, ctx, pool, parentChannelID, memberMemberID, now)
	mustSeedDMScope(t, ctx, pool, workspaceID, dmThreadID, now)
	mustSeedScopeMembership(t, ctx, pool, dmThreadID, memberMemberID, now)
	mustSeedScopeMembership(t, ctx, pool, dmThreadID, peerMemberID, now)
	mustSeedReplyThreadScope(t, ctx, pool, workspaceID, replyChannelID, parentChannelID, "msg-parent", "abcd1234", "thread-abcd1234", now)
	mustSeedReplyThreadScope(t, ctx, pool, workspaceID, dmReplyChannelID, dmThreadID, "msg-dm-parent", "dm123456", "thread-dm123456", now)

	channelAccess, err := LoadAuthorizedHumanThreadAccess(ctx, pool, memberUserID, workspaceID, replyChannelID, HumanAction{
		Resource: ResourceThread,
		Action:   ActionRead,
	})
	require.NoError(t, err)
	require.Equal(t, domain.ThreadTypeChannel, channelAccess.ParentThreadType)
	require.True(t, channelAccess.ParentChannelJoined)
	require.Equal(t, "restricted", channelAccess.ParentChannelName)
	require.Equal(t, "abcd1234", channelAccess.ParentMessageShort)

	_, err = LoadAuthorizedHumanThreadAccess(ctx, pool, guestUserID, workspaceID, replyChannelID, HumanAction{
		Resource: ResourceThread,
		Action:   ActionRead,
	})
	require.ErrorIs(t, err, ErrForbidden)

	mustSeedScopeMembership(t, ctx, pool, replyChannelID, guestMemberID, now)
	_, err = LoadAuthorizedHumanThreadAccess(ctx, pool, guestUserID, workspaceID, replyChannelID, HumanAction{
		Resource: ResourceThread,
		Action:   ActionRead,
	})
	require.ErrorIs(t, err, ErrForbidden)

	dmAccess, err := LoadAuthorizedHumanThreadAccess(ctx, pool, memberUserID, workspaceID, dmReplyChannelID, HumanAction{
		Resource: ResourceThread,
		Action:   ActionPost,
	})
	require.NoError(t, err)
	require.Equal(t, domain.ThreadTypeDM, dmAccess.ParentThreadType)
	require.Equal(t, dmThreadID, dmAccess.ParentChannelID)
	require.True(t, dmAccess.ParentDMParticipant)
	require.Equal(t, peerMemberID, dmAccess.PeerMemberID)
	require.Equal(t, "dm123456", dmAccess.ParentMessageShort)

	_, err = LoadAuthorizedHumanThreadAccess(ctx, pool, guestUserID, workspaceID, dmReplyChannelID, HumanAction{
		Resource: ResourceThread,
		Action:   ActionRead,
	})
	require.ErrorIs(t, err, ErrForbidden)

	mustSeedScopeMembership(t, ctx, pool, dmReplyChannelID, guestMemberID, now)
	childDMAccess, err := LoadAuthorizedHumanThreadAccess(ctx, pool, guestUserID, workspaceID, dmReplyChannelID, HumanAction{
		Resource: ResourceThread,
		Action:   ActionRead,
	})
	require.NoError(t, err)
	require.Equal(t, domain.ThreadTypeDM, childDMAccess.ParentThreadType)
	require.Equal(t, dmThreadID, childDMAccess.ParentChannelID)
	require.True(t, childDMAccess.ParentDMParticipant)
}

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

	ctx := context.Background()
	pool := startAccessTestPool(t, ctx)

	now := time.Date(2026, 3, 20, 13, 0, 0, 0, time.UTC)
	workspaceID := "wsp_access_runtime"
	machineID := "mac_access_runtime"
	agentID := "agt_access_runtime"
	adminUserID := "usr_access_runtime_admin"
	adminMemberID := "mem_access_runtime_admin"
	memberUserID := "usr_access_runtime_member"
	memberMemberID := "mem_access_runtime_member"

	mustExec(t, ctx, pool, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'access-runtime', 'access-runtime', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	mustExec(t, ctx, pool, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES
($1, 'admin@example.com', 'admin', 'Runtime Admin', '', FALSE, 'hash', $3, $3, $3),
($2, 'member@example.com', 'member', 'Runtime Member', '', FALSE, 'hash', $3, $3, $3)
`, adminUserID, memberUserID, now)
	mustExec(t, ctx, pool, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, created_at, updated_at)
VALUES
($1, $3, 'human', 'Runtime Admin', $4, 'admin', $6, $6),
($2, $3, 'human', 'Runtime Member', $5, 'member', $6, $6),
('mem_access_runtime_agent', $3, 'agent', 'Runtime Agent', NULL, NULL, $6, $6)
`, adminMemberID, memberMemberID, workspaceID, adminUserID, memberUserID, now)
	mustExec(t, ctx, pool, `
INSERT INTO machines (id, workspace_id, created_by_user_id, name, api_key, created_at, updated_at)
VALUES ($1, $2, $3, 'runtime-machine', 'machine-key', $4, $4)
`, machineID, workspaceID, adminUserID, now)
	mustExec(t, ctx, pool, `
INSERT INTO agents (
	id, workspace_id, machine_id, member_id, name, runtime, model, reasoning_effort, description,
	auth_token_hash, auto_wake_enabled, status, activity, activity_detail, created_at, updated_at
) VALUES (
	$1, $2, $3, 'mem_access_runtime_agent', 'runtime-agent', 'codex', 'gpt-5.4', 'medium', '',
	NULL, TRUE, 'inactive', 'offline', '', $4, $4
)
`, agentID, workspaceID, machineID, now)

	machineAccess, err := LoadAuthorizedHumanMachineAccess(ctx, pool, adminUserID, workspaceID, machineID, HumanAction{
		Resource: ResourceMachine,
		Action:   ActionManage,
	})
	require.NoError(t, err)
	require.Equal(t, machineID, machineAccess.Machine.MachineID)

	agentAccess, err := LoadAuthorizedHumanAgentAccess(ctx, pool, adminUserID, workspaceID, agentID, HumanAction{
		Resource: ResourceAgent,
		Action:   ActionReadWorkspace,
	})
	require.NoError(t, err)
	require.Equal(t, agentID, agentAccess.Agent.AgentID)
	require.Equal(t, machineID, agentAccess.Agent.MachineID)

	_, err = LoadAuthorizedHumanMachineAccess(ctx, pool, memberUserID, workspaceID, machineID, HumanAction{
		Resource: ResourceMachine,
		Action:   ActionManage,
	})
	require.ErrorIs(t, err, ErrForbidden)

	_, err = LoadAuthorizedHumanAgentAccess(ctx, pool, memberUserID, workspaceID, agentID, HumanAction{
		Resource: ResourceAgent,
		Action:   ActionOperate,
	})
	require.ErrorIs(t, err, ErrForbidden)
}

// TestAuthorizeActiveAgentChannelMembershipForwardReverseMatrix verifies the
// single canonical Agent Task-channel authorizer: a same-workspace active
// Agent+Machine+Member with active channel membership is allowed, and every
// reverse/forgery/cross-workspace/archived/kind-mismatch case fails closed with
// one uniform forbidden result (non-enumerating).
func TestAuthorizeActiveAgentChannelMembershipForwardReverseMatrix(t *testing.T) {
	t.Parallel()
	ctx := context.Background()
	pool := startAccessTestPool(t, ctx)
	now := time.Date(2026, 8, 16, 8, 0, 0, 0, time.UTC)

	workspaceID := "wsp_agent_chan_auth"
	channelID := "chn_agent_chan_auth"
	agentMemberID := "mem_agent_chan_auth"
	agentID := "agt_agent_chan_auth"
	machineID := "mach_agent_chan_auth"
	otherMemberID := "mem_other_agent_chan_auth"
	otherAgentID := "agt_other_agent_chan_auth"
	otherMachineID := "mach_other_agent_chan_auth"
	otherWorkspaceID := "wsp_agent_chan_auth_other"
	ownerUserID := "usr_owner_" + workspaceID

	mustExec(t, ctx, pool, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'agent-chan-auth', 'agent-chan-auth', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	mustSeedChannelScope(t, ctx, pool, workspaceID, channelID, "task-board", "Task Board", "tasks", "public", "", false, now)
	mustExec(t, ctx, pool, `
INSERT INTO members (id, workspace_id, type, display_name, created_at, updated_at)
VALUES ($1, $2, 'agent', 'auth-agent', $3, $3),
       ($4, $2, 'agent', 'other-agent', $3, $3)
`, agentMemberID, workspaceID, now, otherMemberID)
	mustExec(t, ctx, pool, `
INSERT INTO machines (id, workspace_id, created_by_user_id, name, api_key, created_at, updated_at)
VALUES ($1, $2, $5, 'auth-machine', 'k', $3, $3),
       ($4, $2, $5, 'other-machine', 'k2', $3, $3)
`, machineID, workspaceID, now, otherMachineID, ownerUserID)
	mustExec(t, ctx, pool, `
INSERT INTO agents (id, workspace_id, machine_id, member_id, name, runtime, model, description,
  auth_token_hash, auto_wake_enabled, status, activity, activity_detail, created_at, updated_at)
VALUES ($1, $2, $3, $4, 'auth-agent', 'codex', 'gpt', '', NULL, TRUE, 'active', 'sleeping', '', $5, $5),
       ($6, $2, $7, $8, 'other-agent', 'codex', 'gpt', '', NULL, TRUE, 'active', 'sleeping', '', $5, $5)
`, agentID, workspaceID, machineID, agentMemberID, now, otherAgentID, otherMachineID, otherMemberID)
	mustSeedScopeMembership(t, ctx, pool, channelID, agentMemberID, now)

	tests := []struct {
		name      string
		workspace string
		member    string
		channel   string
		wantErr   error
	}{
		{name: "channel member active agent allowed", workspace: workspaceID, member: agentMemberID, channel: channelID, wantErr: nil},
		{name: "non-member forbidden", workspace: workspaceID, member: otherMemberID, channel: channelID, wantErr: ErrForbidden},
		{name: "missing channel forbidden", workspace: workspaceID, member: agentMemberID, channel: "chn_missing", wantErr: ErrForbidden},
		{name: "cross-workspace forbidden", workspace: otherWorkspaceID, member: agentMemberID, channel: channelID, wantErr: ErrForbidden},
		{name: "empty workspace forbidden", workspace: "", member: agentMemberID, channel: channelID, wantErr: ErrForbidden},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			err := AuthorizeActiveAgentChannelMembership(ctx, pool, test.workspace, test.member, test.channel)
			if test.wantErr == nil {
				require.NoError(t, err)
			} else {
				require.ErrorIs(t, err, test.wantErr)
			}
		})
	}
}
