//go:build integration

package teamworkflow

import (
	"context"
	"os"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	coredbtestkit "github.com/tidbcloud/AgentTeam/internal/coredb/testkit"
	storeagent "github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	storeteamworkflow "github.com/tidbcloud/AgentTeam/internal/store/teamworkflow"
	planstore "github.com/tidbcloud/AgentTeam/internal/store/workflow/plan"
	planlang "github.com/tidbcloud/AgentTeam/internal/workflow/plan"
)

var teamWorkflowServiceTiDB coredbtestkit.TiDBCoreDBFixture

func TestMain(m *testing.M) {
	os.Exit(coredbtestkit.RunTestsWithTiDBCoreDBFixture(m, "team_workflow_service", &teamWorkflowServiceTiDB))
}

func TestDirectCreatePublishesIdleRelationAndUpdateExactReplayAfterDraftCASChange(t *testing.T) {
	t.Parallel()
	ctx := context.Background()
	now := time.Date(2026, 8, 21, 9, 0, 0, 0, time.UTC)
	db := teamWorkflowServiceTiDB.RollbackDB(t, ctx)
	fixture := seedDirectTeamWorkflowFixture(t, ctx, db, "direct-replay", now)
	service := New(db)
	plans := planstore.New(db)

	created, err := service.Create(ctx, fixture.Leader, CreateInput{
		TeamID: fixture.TeamID, SourceMessageID: "cmd-create-direct-replay",
		PlanKey: "direct_replay", DisplayName: "Direct replay",
		Source: directTeamWorkflowSourceV1, Layout: []byte(`{"zoom":1}`),
		IdempotencyKey: "create-direct-replay", Now: now,
	})
	require.NoError(t, err)
	require.Equal(t, storeteamworkflow.AuthoringStateIdle, created.TeamWorkflow.AuthoringState)
	require.NotNil(t, created.Plan.CurrentVersion)
	require.NotNil(t, created.Plan.DefaultVersion)
	require.Equal(t, created.Plan.CurrentVersion.ID, created.Plan.DefaultVersion.ID)
	require.Equal(t, int64(1), created.Draft.Revision)

	initialVersions, err := plans.ListVersions(ctx, planstore.ListVersionsParams{
		WorkspaceID: fixture.WorkspaceID, PlanID: created.Plan.ID,
	})
	require.NoError(t, err)
	require.Len(t, initialVersions, 1)
	initialVersionID := initialVersions[0].ID
	initialRelationRevision := created.TeamWorkflow.Revision

	updated, err := service.Update(ctx, fixture.Leader, UpdateInput{
		TeamID: fixture.TeamID, PlanID: created.Plan.ID,
		ExpectedPlanRevision: created.Plan.Revision, DraftRevision: created.Draft.Revision,
		Source: directTeamWorkflowSourceV2, Layout: []byte(`{"zoom":2}`), MakeDefault: false,
		ClientRequestID: "update-direct-replay", Now: now.Add(time.Minute),
	})
	require.NoError(t, err)
	require.False(t, updated.Replayed)
	require.Equal(t, int64(2), *updated.Version.DraftRevision)
	require.Equal(t, initialVersionID, updated.Plan.DefaultVersion.ID)
	require.Equal(t, updated.Version.ID, updated.Plan.CurrentVersion.ID)
	require.Equal(t, initialRelationRevision+1, updated.TeamWorkflow.Revision)

	// A later mutable draft save must not change the command receipt. It is
	// deliberately outside the Team publication service to model another
	// authoring writer advancing the CAS token after the first commit.
	laterDraft, err := plans.SaveDraft(ctx, planstore.SaveDraftParams{
		WorkspaceID: fixture.WorkspaceID, PlanID: created.Plan.ID,
		ExpectedRevision: updated.Draft.Revision, Source: directTeamWorkflowSourceV3,
		Layout: []byte(`{"zoom":3}`), Actor: planstore.Actor{
			Type: planstore.ActorTypeAgent, ID: fixture.Leader.AgentID, MemberID: fixture.Leader.MemberID,
		}, Now: now.Add(2 * time.Minute),
	})
	require.NoError(t, err)
	require.Equal(t, updated.Draft.Revision+1, laterDraft.Revision)
	metadata := "Direct replay metadata"
	planAfterMetadata, err := plans.UpdatePlanMetadata(ctx, planstore.UpdatePlanMetadataParams{
		WorkspaceID: fixture.WorkspaceID, PlanID: created.Plan.ID,
		ExpectedRevision: updated.Plan.Revision, DisplayName: &metadata,
		Actor: planstore.Actor{Type: planstore.ActorTypeAgent, ID: fixture.Leader.AgentID, MemberID: fixture.Leader.MemberID},
		Now:   now.Add(3 * time.Minute),
	})
	require.NoError(t, err)
	require.Equal(t, updated.Plan.Revision+1, planAfterMetadata.Revision)

	replayed, err := service.Update(ctx, fixture.Leader, UpdateInput{
		TeamID: fixture.TeamID, PlanID: created.Plan.ID,
		ExpectedPlanRevision: created.Plan.Revision, DraftRevision: created.Draft.Revision,
		Source: directTeamWorkflowSourceV2, Layout: []byte(`{"zoom":2}`), MakeDefault: false,
		ClientRequestID: "update-direct-replay", Now: now.Add(3 * time.Minute),
	})
	require.NoError(t, err)
	require.True(t, replayed.Replayed)
	require.Equal(t, updated.Version.ID, replayed.Version.ID)
	require.Equal(t, updated.TeamWorkflow.Revision, replayed.TeamWorkflow.Revision)
	require.Equal(t, planAfterMetadata.Revision, replayed.Plan.Revision)
	versionsAfterReplay, err := plans.ListVersions(ctx, planstore.ListVersionsParams{
		WorkspaceID: fixture.WorkspaceID, PlanID: created.Plan.ID,
	})
	require.NoError(t, err)
	require.Len(t, versionsAfterReplay, 2)

	for _, test := range []struct {
		name    string
		changed UpdateInput
	}{
		{name: "source", changed: UpdateInput{
			TeamID: fixture.TeamID, PlanID: created.Plan.ID,
			ExpectedPlanRevision: created.Plan.Revision, DraftRevision: created.Draft.Revision,
			Source: directTeamWorkflowSourceV3, Layout: []byte(`{"zoom":2}`),
			ClientRequestID: "update-direct-replay",
		}},
		{name: "layout", changed: UpdateInput{
			TeamID: fixture.TeamID, PlanID: created.Plan.ID,
			ExpectedPlanRevision: created.Plan.Revision, DraftRevision: created.Draft.Revision,
			Source: directTeamWorkflowSourceV2, Layout: []byte(`{"zoom":3}`),
			ClientRequestID: "update-direct-replay",
		}},
	} {
		t.Run(test.name, func(t *testing.T) {
			_, err := service.Update(ctx, fixture.Leader, test.changed)
			var conflict planstore.IdempotencyConflictError
			require.ErrorAs(t, err, &conflict)
		})
	}

	stalePlan := UpdateInput{
		TeamID: fixture.TeamID, PlanID: created.Plan.ID,
		ExpectedPlanRevision: created.Plan.Revision - 1, DraftRevision: laterDraft.Revision,
		Source: directTeamWorkflowSourceV3, ClientRequestID: "stale-plan",
	}
	_, err = service.Update(ctx, fixture.Leader, stalePlan)
	require.ErrorIs(t, err, planstore.ErrConflict)
	staleDraft := stalePlan
	staleDraft.ExpectedPlanRevision = created.Plan.Revision
	staleDraft.DraftRevision = updated.Draft.Revision
	staleDraft.ClientRequestID = "stale-draft"
	_, err = service.Update(ctx, fixture.Leader, staleDraft)
	require.ErrorIs(t, err, planstore.ErrConflict)
	versionsAfterStale, err := plans.ListVersions(ctx, planstore.ListVersionsParams{
		WorkspaceID: fixture.WorkspaceID, PlanID: created.Plan.ID,
	})
	require.NoError(t, err)
	require.Len(t, versionsAfterStale, 2)

	changedActor := fixture.Leader
	changedActor.AgentID = "agt_direct_replay_other"
	_, err = service.Update(ctx, changedActor, UpdateInput{
		TeamID: fixture.TeamID, PlanID: created.Plan.ID,
		ExpectedPlanRevision: created.Plan.Revision, DraftRevision: created.Draft.Revision,
		Source: directTeamWorkflowSourceV2, Layout: []byte(`{"zoom":2}`),
		ClientRequestID: "update-direct-replay",
	})
	var actorConflict planstore.IdempotencyConflictError
	require.ErrorAs(t, err, &actorConflict)
}

func TestDirectCreateAuthorizationAndCompileFailureAreAtomic(t *testing.T) {
	t.Parallel()
	ctx := context.Background()
	now := time.Date(2026, 8, 21, 10, 0, 0, 0, time.UTC)
	for _, test := range []struct {
		name      string
		configure func(*Service, teamWorkflowServiceFixture)
		principal func(teamWorkflowServiceFixture) storeagent.AuthenticatedAgent
		wantErr   error
	}{
		{
			name: "non_leader",
			principal: func(fixture teamWorkflowServiceFixture) storeagent.AuthenticatedAgent {
				principal := fixture.Leader
				principal.AgentID = "agt_direct_other"
				principal.MemberID = "mem_direct_other"
				return principal
			},
			wantErr: storeerr.ErrForbidden,
		},
		{
			name:      "compile_failure",
			principal: func(fixture teamWorkflowServiceFixture) storeagent.AuthenticatedAgent { return fixture.Leader },
			configure: func(service *Service, _ teamWorkflowServiceFixture) {
				service.compile = func([]byte) (*planlang.Artifact, []planlang.Diagnostic, error) {
					return nil, []planlang.Diagnostic{{Severity: planlang.SeverityError, Code: "schema.invalid", Message: "bad plan"}}, nil
				}
			},
			wantErr: storeerr.ErrUnprocessable,
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()
			db := teamWorkflowServiceTiDB.RollbackDB(t, ctx)
			fixture := seedDirectTeamWorkflowFixture(t, ctx, db, "atomic-"+test.name, now)
			service := New(db)
			if test.configure != nil {
				test.configure(service, fixture)
			}
			_, err := service.Create(ctx, test.principal(fixture), CreateInput{
				TeamID: fixture.TeamID, SourceMessageID: "cmd-atomic-" + test.name,
				PlanKey: "atomic_" + test.name, DisplayName: "Atomic",
				Source: directTeamWorkflowSourceV1, Layout: []byte(`{}`),
				IdempotencyKey: "create-atomic-" + test.name, Now: now,
			})
			require.ErrorIs(t, err, test.wantErr)
			var relationCount, planCount int
			require.NoError(t, db.QueryRow(ctx, `SELECT COUNT(*) FROM workspace_team_workflows WHERE team_id=$1`, fixture.TeamID).Scan(&relationCount))
			require.NoError(t, db.QueryRow(ctx, `SELECT COUNT(*) FROM workflow_plans WHERE plan_key=$1`, "atomic_"+test.name).Scan(&planCount))
			require.Zero(t, relationCount)
			require.Zero(t, planCount)
		})
	}
}

// The fixture intentionally contains only the rows needed by the Team leader
// authorization and Workflow Plan/Team relation stores. It is reused by the
// direct publication behavior tests instead of restoring the old approval
// lifecycle fixture and its generic approval ledger assertions.
type teamWorkflowServiceFixture struct {
	WorkspaceID string
	TeamID      string
	ChannelID   string
	Leader      storeagent.AuthenticatedAgent
}

func seedDirectTeamWorkflowFixture(t *testing.T, ctx context.Context, db coredb.DB, suffix string, now time.Time) teamWorkflowServiceFixture {
	t.Helper()
	fixture := teamWorkflowServiceFixture{
		WorkspaceID: "wsp_direct_" + suffix,
		TeamID:      "team_direct_" + suffix,
		ChannelID:   "chn_direct_" + suffix,
		Leader: storeagent.AuthenticatedAgent{
			WorkspaceID: "wsp_direct_" + suffix,
			AgentID:     "agt_direct_leader_" + suffix,
			MemberID:    "mem_direct_leader_" + suffix,
			MachineID:   "mac_direct_" + suffix,
		},
	}
	ownerMemberID := "mem_direct_owner_" + suffix
	ownerUserID := "usr_direct_owner_" + suffix
	organizationID := "org_direct_" + suffix
	execDirectFixture(t, ctx, db, `
INSERT INTO users (id, email, name, display_name, password_hash, created_at, updated_at)
VALUES ($1,$2,$1,$1,'hash',$3,$3)
`, ownerUserID, ownerUserID+"@example.com", now)
	execDirectFixture(t, ctx, db, `
INSERT INTO organizations (id, slug, name, created_by_user_id, created_at, updated_at)
VALUES ($1,$2,$2,$3,$4,$4)
`, organizationID, "direct-"+suffix, ownerUserID, now)
	execDirectFixture(t, ctx, db, `
INSERT INTO workspaces (id, organization_id, slug, name, created_at, updated_at)
VALUES ($1,$2,$3,$3,$4,$4)
`, fixture.WorkspaceID, organizationID, "direct-"+suffix, now)
	execDirectFixture(t, ctx, db, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES
 ($1,$3,'human',$1,$4,'owner','active',$5,$5),
 ($2,$3,'agent',$2,NULL,NULL,'active',$5,$5)
`, ownerMemberID, fixture.Leader.MemberID, fixture.WorkspaceID, ownerUserID, now)
	execDirectFixture(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)
`, fixture.Leader.MachineID, fixture.WorkspaceID, ownerUserID, "key-"+fixture.Leader.MachineID, now)
	execDirectFixture(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,$1,'','codex','gpt-5','',NULL,'',$5,$5)
`, fixture.Leader.AgentID, fixture.WorkspaceID, fixture.Leader.MachineID, fixture.Leader.MemberID, now)
	execDirectFixture(t, ctx, db, `
INSERT INTO workspace_teams (
 id, workspace_id, handle, display_name, description, avatar_url, visibility, status,
 channel_id, created_by_member_id, created_at, updated_at, archived_at, active_handle_key
) VALUES ($1,$2,$3,$3,'','','private','active',$4,$5,$6,$6,NULL,$7)
`, fixture.TeamID, fixture.WorkspaceID, "team-"+suffix, fixture.ChannelID, ownerMemberID, now,
		fixture.WorkspaceID+":team-"+suffix)
	execDirectFixture(t, ctx, db, `
INSERT INTO workspace_team_members (
 id, workspace_id, team_id, actor_kind, actor_id, role, role_label,
 role_overlay_prompt, role_overlay_version, wake_on_team_mention, sort_order,
 added_by_member_id, added_at, updated_at, removed_at, active_member_key,
 active_agent_team_key, active_leader_key
) VALUES ($1,$2,$3,'agent',$4,'leader','','',1,TRUE,10,$5,$6,$6,NULL,$7,$8,$9)
`, "wtm_direct_"+suffix, fixture.WorkspaceID, fixture.TeamID, fixture.Leader.MemberID,
		ownerMemberID, now, fixture.TeamID+":agent:"+fixture.Leader.MemberID,
		fixture.WorkspaceID+":agent:"+fixture.Leader.MemberID, fixture.WorkspaceID+":"+fixture.TeamID)
	return fixture
}

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

const directTeamWorkflowSourceV1 = `apiVersion: loop.plan/v1
kind: PlanVersion
graph:
  entry: {nodeId: pause, port: in}
  nodes:
  - id: pause
    type: wait
    wait:
      until: {value: PT1M}
      timeout: PT2M
      retry: {maxAttempts: 2, backoff: 1s}
  - id: done
    type: end
    end: {outcome: succeeded}
  - id: failed
    type: end
    end: {outcome: failed, code: wait_failed}
  edges:
  - id: completed
    source: {nodeId: pause, port: completed}
    target: {nodeId: done, port: in}
  - id: timed_out
    source: {nodeId: pause, port: timed_out}
    target: {nodeId: failed, port: in}
  - id: action_failed
    source: {nodeId: pause, port: failed}
    target: {nodeId: failed, port: in}`

const directTeamWorkflowSourceV2 = `apiVersion: loop.plan/v1
kind: PlanVersion
graph:
  entry: {nodeId: pause, port: in}
  nodes:
  - id: pause
    type: wait
    wait:
      until: {value: PT2M}
      timeout: PT3M
      retry: {maxAttempts: 3, backoff: 1s}
  - id: done
    type: end
    end: {outcome: succeeded}
  - id: failed
    type: end
    end: {outcome: failed, code: wait_failed}
  edges:
  - id: completed
    source: {nodeId: pause, port: completed}
    target: {nodeId: done, port: in}
  - id: timed_out
    source: {nodeId: pause, port: timed_out}
    target: {nodeId: failed, port: in}
  - id: action_failed
    source: {nodeId: pause, port: failed}
    target: {nodeId: failed, port: in}`

const directTeamWorkflowSourceV3 = `apiVersion: loop.plan/v1
kind: PlanVersion
graph:
  entry: {nodeId: pause, port: in}
  nodes:
  - id: pause
    type: wait
    wait:
      until: {value: PT3M}
      timeout: PT4M
      retry: {maxAttempts: 4, backoff: 1s}
  - id: done
    type: end
    end: {outcome: succeeded}
  - id: failed
    type: end
    end: {outcome: failed, code: wait_failed}
  edges:
  - id: completed
    source: {nodeId: pause, port: completed}
    target: {nodeId: done, port: in}
  - id: timed_out
    source: {nodeId: pause, port: timed_out}
    target: {nodeId: failed, port: in}
  - id: action_failed
    source: {nodeId: pause, port: failed}
    target: {nodeId: failed, port: in}`
