package access

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/testfixture"
)

// taskBoardChannelFailClosedFixture seeds one fully isolated workspace /
// channel / board / task / Agent / Machine / Member namespace on a fresh
// rollback-isolated pool. Every case builds its own fixture so cumulative state
// from an earlier mutation can never mask a later failure (each case asserts a
// clean precondition and applies exactly one intended mutation before denial).
type taskBoardChannelFailClosedFixture struct {
	pool          coredb.QueryExecer
	workspaceID   string
	channelID     string
	boardID       string
	taskScopeID   string
	taskID        string
	agentMemberID string
	agentID       string
	machineID     string
	ownerUserID   string
	ownerMemberID string
}

func seedTaskBoardChannelFailClosedFixture(t *testing.T, ctx context.Context, suffix string) taskBoardChannelFailClosedFixture {
	t.Helper()
	pool := startAccessTestPool(t, ctx)
	now := time.Date(2026, 8, 16, 9, 0, 0, 0, time.UTC)

	fixture := taskBoardChannelFailClosedFixture{
		pool:          pool,
		workspaceID:   "wsp_fc_" + suffix,
		channelID:     "chn_fc_" + suffix,
		boardID:       "brd_fc_" + suffix,
		taskScopeID:   "tsk_fc_" + suffix,
		taskID:        "tsk_fc_" + suffix,
		agentMemberID: "mem_fc_" + suffix,
		agentID:       "agt_fc_" + suffix,
		machineID:     "mac_fc_" + suffix,
		ownerUserID:   "usr_fc_" + suffix,
		ownerMemberID: "mem_fc_" + suffix + "_human",
	}

	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, $2, $2, $2, '', FALSE, 'hash', $3, $3, $3)
`, fixture.ownerUserID, "fc-"+suffix+"@example.com", now)
	mustExec(t, ctx, pool, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, $3, $3, 0, $4, $4)
`, fixture.workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, fixture.workspaceID, now), suffix, now)
	mustSeedChannelScope(t, ctx, pool, fixture.workspaceID, fixture.channelID, "fc-"+suffix, "FC "+suffix, "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', 'fc-agent', $3, $3),
       ($4, $2, 'human', 'fc-owner', $3, $3)
`, fixture.agentMemberID, fixture.workspaceID, now, fixture.ownerMemberID)
	mustExec(t, ctx, pool, `
INSERT INTO machines (id, workspace_id, created_by_user_id, name, api_key, created_at, updated_at)
VALUES ($1, $2, $4, 'fc-machine', 'k', $3, $3)
`, fixture.machineID, fixture.workspaceID, now, fixture.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, 'fc-agent', 'codex', 'gpt', '', NULL, TRUE, 'active', 'sleeping', '', $5, $5)
`, fixture.agentID, fixture.workspaceID, fixture.machineID, fixture.agentMemberID, now)
	mustSeedScopeMembership(t, ctx, pool, fixture.channelID, fixture.agentMemberID, now)
	mustSeedTaskScope(t, ctx, pool, fixture.workspaceID, fixture.taskScopeID, now)
	mustSeedTaskBoard(t, ctx, pool, fixture.workspaceID, fixture.channelID, fixture.boardID, now)
	mustSeedTaskRow(t, ctx, pool, fixture.workspaceID, fixture.channelID, fixture.boardID, fixture.taskScopeID, fixture.taskID, fixture.ownerMemberID, now)
	return fixture
}

// TestResolveActiveTaskBoardChannelFailClosedMatrix proves the canonical
// Task->board Channel resolver: the board's authoritative Channel is derived
// from tasks.board_id -> task_boards (never the denormalized tasks.channel_id
// alone), and every fail-closed case uses its own isolated fixture with a clean
// precondition and exactly one intended mutation before denial, so cumulative
// state cannot mask failures.
func TestResolveActiveTaskBoardChannelFailClosedMatrix(t *testing.T) {
	t.Parallel()
	ctx := context.Background()

	// Forward: a fresh fixture resolves the board channel and authorizes the
	// active Agent+Machine+Member with no Task membership.
	forward := seedTaskBoardChannelFailClosedFixture(t, ctx, "fwd")
	resolved, err := ResolveActiveTaskBoardChannel(ctx, forward.pool, forward.workspaceID, forward.taskScopeID)
	require.NoError(t, err)
	require.Equal(t, forward.channelID, resolved.ChannelID)
	require.Equal(t, forward.taskScopeID, resolved.TaskScopeID)
	_, err = AuthorizeActiveAgentTaskBoardChannel(ctx, forward.pool, forward.workspaceID, forward.agentMemberID, forward.taskScopeID)
	require.NoError(t, err)

	t.Run("missing_task_scope", func(t *testing.T) {
		f := seedTaskBoardChannelFailClosedFixture(t, ctx, "missing")
		_, err := ResolveActiveTaskBoardChannel(ctx, f.pool, f.workspaceID, "tsk_does_not_exist")
		require.ErrorIs(t, err, ErrForbidden)
	})

	t.Run("cross_workspace", func(t *testing.T) {
		f := seedTaskBoardChannelFailClosedFixture(t, ctx, "xws")
		other := seedTaskBoardChannelFailClosedFixture(t, ctx, "xws_other")
		// The task exists in workspace A; query from workspace B -> forbidden.
		_, err := ResolveActiveTaskBoardChannel(ctx, other.pool, other.workspaceID, f.taskScopeID)
		require.ErrorIs(t, err, ErrForbidden)
	})

	t.Run("null_orphan_board", func(t *testing.T) {
		f := seedTaskBoardChannelFailClosedFixture(t, ctx, "orphan")
		// Exactly one intended mutation: detach the board from the task.
		mustExec(t, ctx, f.pool, `UPDATE tasks SET board_id = NULL WHERE id = $1`, f.taskID)
		_, err := ResolveActiveTaskBoardChannel(ctx, f.pool, f.workspaceID, f.taskScopeID)
		require.ErrorIs(t, err, ErrForbidden)
	})

	t.Run("deleted_task_scope", func(t *testing.T) {
		f := seedTaskBoardChannelFailClosedFixture(t, ctx, "deleted")
		// Exactly one intended mutation: archive/delete the Task scope.
		now := time.Now().UTC()
		mustExec(t, ctx, f.pool, `UPDATE scopes SET status = 'deleted', archived_at = $2 WHERE id = $1`, f.taskScopeID, now)
		_, err := ResolveActiveTaskBoardChannel(ctx, f.pool, f.workspaceID, f.taskScopeID)
		require.ErrorIs(t, err, ErrForbidden)
	})

	t.Run("deleted_channel", func(t *testing.T) {
		f := seedTaskBoardChannelFailClosedFixture(t, ctx, "delchn")
		now := time.Now().UTC()
		mustExec(t, ctx, f.pool, `UPDATE scopes SET status = 'deleted', archived_at = $2 WHERE id = $1`, f.channelID, now)
		_, err := ResolveActiveTaskBoardChannel(ctx, f.pool, f.workspaceID, f.taskScopeID)
		require.ErrorIs(t, err, ErrForbidden)
	})

	t.Run("archived_channel", func(t *testing.T) {
		f := seedTaskBoardChannelFailClosedFixture(t, ctx, "archchn")
		now := time.Now().UTC()
		mustExec(t, ctx, f.pool, `UPDATE scopes SET archived_at = $2, updated_at = $2 WHERE id = $1`, f.channelID, now)
		_, err := ResolveActiveTaskBoardChannel(ctx, f.pool, f.workspaceID, f.taskScopeID)
		require.ErrorIs(t, err, ErrForbidden)
	})

	t.Run("inactive_agent", func(t *testing.T) {
		f := seedTaskBoardChannelFailClosedFixture(t, ctx, "inagent")
		mustExec(t, ctx, f.pool, `UPDATE agents SET status = 'inactive' WHERE id = $1`, f.agentID)
		_, err := AuthorizeActiveAgentTaskBoardChannel(ctx, f.pool, f.workspaceID, f.agentMemberID, f.taskScopeID)
		require.NoError(t, err)
	})

	t.Run("archived_agent", func(t *testing.T) {
		f := seedTaskBoardChannelFailClosedFixture(t, ctx, "archagent")
		mustExec(t, ctx, f.pool, `UPDATE agents SET status = 'archived' WHERE id = $1`, f.agentID)
		_, err := AuthorizeActiveAgentTaskBoardChannel(ctx, f.pool, f.workspaceID, f.agentMemberID, f.taskScopeID)
		require.NoError(t, err)
	})

	t.Run("inactive_machine", func(t *testing.T) {
		f := seedTaskBoardChannelFailClosedFixture(t, ctx, "inmac")
		mustExec(t, ctx, f.pool, `UPDATE machines SET status = 'inactive' WHERE id = $1`, f.machineID)
		_, err := AuthorizeActiveAgentTaskBoardChannel(ctx, f.pool, f.workspaceID, f.agentMemberID, f.taskScopeID)
		require.ErrorIs(t, err, ErrForbidden)
	})

	t.Run("inactive_member", func(t *testing.T) {
		f := seedTaskBoardChannelFailClosedFixture(t, ctx, "inmem")
		mustExec(t, ctx, f.pool, `UPDATE members SET status = 'inactive' WHERE id = $1`, f.agentMemberID)
		_, err := AuthorizeActiveAgentTaskBoardChannel(ctx, f.pool, f.workspaceID, f.agentMemberID, f.taskScopeID)
		require.ErrorIs(t, err, ErrForbidden)
	})

	t.Run("kind_mismatch", func(t *testing.T) {
		f := seedTaskBoardChannelFailClosedFixture(t, ctx, "kind")
		mustExec(t, ctx, f.pool, `UPDATE scopes SET type = 'channel' WHERE id = $1`, f.taskScopeID)
		_, err := ResolveActiveTaskBoardChannel(ctx, f.pool, f.workspaceID, f.taskScopeID)
		require.ErrorIs(t, err, ErrForbidden)
	})

	t.Run("board_channel_mismatch", func(t *testing.T) {
		f := seedTaskBoardChannelFailClosedFixture(t, ctx, "mismatch")
		otherChannelID := "chn_fc_mismatch_other"
		otherBoardID := "brd_fc_mismatch_other"
		mustSeedChannelScope(t, ctx, f.pool, f.workspaceID, otherChannelID, "fc-mismatch-other", "FC Mismatch Other", "tasks", "public", "", false, time.Now().UTC())
		mustSeedTaskBoard(t, ctx, f.pool, f.workspaceID, otherChannelID, otherBoardID, time.Now().UTC())
		// Exactly one intended mutation: repoint board_id to a board whose
		// channel differs from the task's denormalized channel_id.
		mustExec(t, ctx, f.pool, `UPDATE tasks SET board_id = $2 WHERE id = $1`, f.taskID, otherBoardID)
		_, err := ResolveActiveTaskBoardChannel(ctx, f.pool, f.workspaceID, f.taskScopeID)
		require.ErrorIs(t, err, ErrForbidden)
	})

	t.Run("revoke_channel_retains_task_membership", func(t *testing.T) {
		f := seedTaskBoardChannelFailClosedFixture(t, ctx, "revoke")
		// Retain a direct Task membership, then revoke the board Channel.
		mustExec(t, ctx, f.pool, `
INSERT INTO scope_memberships (
  scope_id, member_id, visibility, owner_member_id, last_read_seq, last_read_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, 'private', $3, 0, 0, 0, 1, NULL, $4, $4, NULL, NULL)
`, f.taskScopeID, f.agentMemberID, f.ownerMemberID, time.Now().UTC())
		mustExec(t, ctx, f.pool, `
UPDATE scope_memberships SET removed_at = $3, updated_at = $3 WHERE scope_id = $1 AND member_id = $2 AND removed_at IS NULL
`, f.channelID, f.agentMemberID, time.Now().UTC())
		_, err := AuthorizeActiveAgentTaskBoardChannel(ctx, f.pool, f.workspaceID, f.agentMemberID, f.taskScopeID)
		require.ErrorIs(t, err, ErrForbidden)
	})
}
