package main

import (
	"bytes"
	"context"
	"database/sql"
	"fmt"
	"io"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/config"
	"github.com/tidbcloud/AgentTeam/internal/coredb/testkit"
	"github.com/tidbcloud/AgentTeam/internal/devsearch"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
)

func TestRunWorkspaceMessageSearchCommandsManageWorkspaceGlobalProjection(t *testing.T) {
	ctx, dsn, db := startMessageSearchCommandTestDB(t, testkit.TiDBTestRuntimeUnistore)
	seedScopeMessageSearchCommandMessages(t, ctx, db)
	seedAdditionalScopeMessageSearchCommandMessages(t, ctx, db)
	t.Setenv("AGENTTEAM_DATABASE_DSN", dsn)
	t.Setenv("AGENTTEAM_MESSAGE_SEARCH_BACKFILL_BATCH_SIZE", "1")
	t.Setenv("AGENTTEAM_DATABASE_CONNECT_TIMEOUT", "5s")
	t.Setenv(workspaceMessageSearchWorkspaceIDEnv, "wsp_scope_cmd_backfill")

	require.NoError(t, runWorkspaceMessageSearchBackfill(ctx))

	var projectionCount int
	require.NoError(t, db.QueryRowContext(ctx, `
SELECT COUNT(*)
FROM scope_message_search_docs
WHERE workspace_id = 'wsp_scope_cmd_backfill'
`).Scan(&projectionCount))
	require.Equal(t, 3, projectionCount)

	err := runWorkspaceMessageSearchFinalize(ctx)
	require.Error(t, err)
	require.ErrorContains(t, err, "tidb_enable_fulltext_index is not enabled")
	require.ErrorContains(t, runWorkspaceMessageSearchPromoteReady(ctx), "scope search finalize must be complete before promote-ready")

	output := captureStdout(t, func() {
		require.NoError(t, runWorkspaceMessageSearchStatus(ctx))
	})
	require.JSONEq(t, `{
		"workspace_id": "wsp_scope_cmd_backfill",
		"backfill_status": "complete",
		"backfill_cursor_global_seq": 3,
		"finalize_status": "pending",
		"read_ready": false
	}`, strings.TrimSpace(output))
}

func TestCurrentWorkspaceMessageSearchTargetRequiresWorkspaceID(t *testing.T) {
	t.Setenv(workspaceMessageSearchWorkspaceIDEnv, "")

	_, err := currentWorkspaceMessageSearchTarget()
	require.ErrorContains(t, err, workspaceMessageSearchWorkspaceIDEnv)
}

func TestCurrentWorkspaceMessageSearchTargetReturnsWorkspaceID(t *testing.T) {
	t.Setenv(workspaceMessageSearchWorkspaceIDEnv, "wsp_scope_cmd_global")

	target, err := currentWorkspaceMessageSearchTarget()
	require.NoError(t, err)
	require.Equal(t, "wsp_scope_cmd_global", target.workspaceID)
}

func TestRunWorkspaceMessageSearchEnsureReadyPrintsFinalJSONOnFailure(t *testing.T) {
	ctx, dsn, _ := startMessageSearchCommandTestDB(t, testkit.TiDBTestRuntimeUnistore)
	t.Setenv("AGENTTEAM_DATABASE_DSN", dsn)
	t.Setenv("AGENTTEAM_DATABASE_CONNECT_TIMEOUT", "5s")
	t.Setenv("AGENTTEAM_MESSAGE_SEARCH_BACKFILL_BATCH_SIZE", "7")

	orig := ensureAllWorkspacesSearchReadyFn
	t.Cleanup(func() {
		ensureAllWorkspacesSearchReadyFn = orig
	})
	ensureAllWorkspacesSearchReadyFn = func(_ context.Context, store devsearch.SearchReadinessStore, batchSize int, logw *os.File) (devsearch.EnsureAllWorkspacesResult, error) {
		require.NotNil(t, store)
		require.Equal(t, 7, batchSize)
		require.NotNil(t, logw)
		return devsearch.EnsureAllWorkspacesResult{
			Ready:          false,
			FastPath:       false,
			WorkspaceCount: 2,
			ProcessedCount: 1,
			CurrentPhase:   "finalize",
			Error:          "boom",
			Workspaces: []devsearch.WorkspaceSearchReadyResult{
				{
					WorkspaceID:             "wsp_scope_cmd_backfill",
					BackfillStatus:          "complete",
					BackfillCursorGlobalSeq: 3,
					FinalizeStatus:          "pending",
					ReadReady:               false,
				},
			},
		}, fmt.Errorf("boom")
	}

	var runErr error
	output := captureStdout(t, func() {
		runErr = runWorkspaceMessageSearchEnsureReady(ctx)
	})
	require.ErrorContains(t, runErr, "boom")
	require.JSONEq(t, `{
		"ready": false,
		"fast_path": false,
		"workspace_count": 2,
		"processed_count": 1,
		"current_phase": "finalize",
		"error": "boom",
		"workspaces": [
			{
				"workspace_id": "wsp_scope_cmd_backfill",
				"backfill_status": "complete",
				"backfill_cursor_global_seq": 3,
				"finalize_status": "pending",
				"read_ready": false
			}
		]
	}`, strings.TrimSpace(output))
}

func seedScopeMessageSearchCommandMessages(t *testing.T, ctx context.Context, db *sql.DB) {
	t.Helper()

	now := time.Date(2026, 5, 13, 6, 0, 0, 0, time.UTC)
	const (
		userID         = "usr_scope_cmd_backfill"
		organizationID = "org_scope_cmd_backfill"
		workspaceID    = "wsp_scope_cmd_backfill"
		memberID       = "mem_scope_cmd_backfill"
		scopeID        = "scp_scope_cmd_backfill"
	)

	_, err := db.ExecContext(ctx, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES (?, 'scope-cmd-backfill@example.com', 'scope-cmd-backfill', 'Scope Cmd Backfill', '', FALSE, 'hash', ?, ?, ?)
`, userID, now, now, now)
	require.NoError(t, err)
	_, err = db.ExecContext(ctx, `
INSERT INTO organizations (id, slug, name, created_by_user_id, created_at, updated_at)
VALUES (?, 'scope-cmd-backfill', 'scope-cmd-backfill', ?, ?, ?)
`, organizationID, userID, now, now)
	require.NoError(t, err)
	_, err = db.ExecContext(ctx, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES (?, ?, 'scope-cmd-backfill', 'scope-cmd-backfill', 2, ?, ?)
`, workspaceID, organizationID, now, now)
	require.NoError(t, err)
	_, err = db.ExecContext(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES (?, ?, 'human', 'Scope Cmd Backfill', ?, 'owner', 'active', ?, ?)
`, memberID, workspaceID, userID, now, now)
	require.NoError(t, err)
	_, err = db.ExecContext(ctx, `
INSERT INTO scopes (
  id, workspace_id, type, identity_key, latest_seq, latest_global_seq,
  latest_message_at, status, created_by_member_id, created_at, updated_at
) VALUES (?, ?, 'channel', 'scope-cmd-backfill', 2, 2, ?, 'active', ?, ?, ?)
`, scopeID, workspaceID, now.Add(time.Minute), memberID, now, now)
	require.NoError(t, err)
	_, err = db.ExecContext(ctx, `
INSERT INTO scope_memberships (scope_id, member_id, visibility, effective_from_seq, created_at, updated_at)
VALUES (?, ?, 'public', 1, ?, ?)
`, scopeID, memberID, now, now)
	require.NoError(t, err)
	for seq, content := range []string{"scope rollout needle one", "scope rollout needle two"} {
		_, err = db.ExecContext(ctx, `
INSERT INTO scope_messages (
  id, workspace_id, scope_id, seq, workspace_global_seq, author_member_id,
  author_type, message_type, content, metadata, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, 'human', 'text', ?, NULL, ?, ?)
`, fmt.Sprintf("msg_scope_cmd_backfill_%d", seq+1), workspaceID, scopeID, seq+1, seq+1, memberID, content, now.Add(time.Duration(seq)*time.Minute), now.Add(time.Duration(seq)*time.Minute))
		require.NoError(t, err)
	}
}

func seedAdditionalScopeMessageSearchCommandMessages(t *testing.T, ctx context.Context, db *sql.DB) {
	t.Helper()

	now := time.Date(2026, 5, 13, 6, 10, 0, 0, time.UTC)
	const (
		workspaceID = "wsp_scope_cmd_backfill"
		memberID    = "mem_scope_cmd_backfill"
		scopeID     = "scp_scope_cmd_backfill_other"
	)

	_, err := db.ExecContext(ctx, `
INSERT INTO scopes (
  id, workspace_id, type, identity_key, latest_seq, latest_global_seq,
  latest_message_at, status, created_by_member_id, created_at, updated_at
) VALUES (?, ?, 'channel', 'scope-cmd-backfill-other', 1, 3, ?, 'active', ?, ?, ?)
`, scopeID, workspaceID, now, memberID, now, now)
	require.NoError(t, err)
	_, err = db.ExecContext(ctx, `
INSERT INTO scope_memberships (scope_id, member_id, visibility, effective_from_seq, created_at, updated_at)
VALUES (?, ?, 'public', 1, ?, ?)
`, scopeID, memberID, now, now)
	require.NoError(t, err)
	_, err = db.ExecContext(ctx, `
INSERT INTO scope_messages (
  id, workspace_id, scope_id, seq, workspace_global_seq, author_member_id,
  author_type, message_type, content, metadata, created_at, updated_at
) VALUES (?, ?, ?, 1, 3, ?, 'human', 'text', ?, NULL, ?, ?)
`, "msg_scope_cmd_backfill_3", workspaceID, scopeID, memberID, "scope rollout needle three", now, now)
	require.NoError(t, err)
}

func captureStdout(t *testing.T, fn func()) string {
	t.Helper()

	originalStdout := os.Stdout
	reader, writer, err := os.Pipe()
	require.NoError(t, err)

	os.Stdout = writer
	defer func() {
		os.Stdout = originalStdout
	}()

	fn()

	require.NoError(t, writer.Close())

	output, err := io.ReadAll(reader)
	require.NoError(t, err)
	require.NoError(t, reader.Close())
	return string(output)
}

func captureStderr(t *testing.T, fn func()) string {
	t.Helper()

	originalStderr := os.Stderr
	reader, writer, err := os.Pipe()
	require.NoError(t, err)

	os.Stderr = writer
	defer func() {
		os.Stderr = originalStderr
	}()

	fn()

	require.NoError(t, writer.Close())

	output, err := io.ReadAll(reader)
	require.NoError(t, err)
	require.NoError(t, reader.Close())
	return string(output)
}

func TestParseWorkspacePlacementGroupID(t *testing.T) {
	tests := []struct {
		name              string
		parse             func([]string) (string, error)
		defaultGroupID    string
		requiredErr       string
		unexpectedArgsErr string
	}{
		{
			name:              "backfill",
			parse:             parseWorkspacePlacementBackfillGroupID,
			defaultGroupID:    config.DefaultWorkspaceGroupID,
			unexpectedArgsErr: "unexpected workspace-placement-backfill arguments: extra",
		},
		{
			name:              "verify",
			parse:             parseWorkspacePlacementVerifyGroupID,
			requiredErr:       "workspace-placement-verify requires --workspace-group-id or AGENTTEAM_WORKSPACE_GROUP_ID",
			unexpectedArgsErr: "unexpected workspace-placement-verify arguments: extra",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name+"/flag", func(t *testing.T) {
			t.Setenv("AGENTTEAM_WORKSPACE_GROUP_ID", "env-group")
			groupID, err := tt.parse([]string{"--workspace-group-id", " flag-group "})
			require.NoError(t, err)
			require.Equal(t, "flag-group", groupID)
		})

		t.Run(tt.name+"/env", func(t *testing.T) {
			t.Setenv("AGENTTEAM_WORKSPACE_GROUP_ID", " env-group ")
			groupID, err := tt.parse(nil)
			require.NoError(t, err)
			require.Equal(t, "env-group", groupID)
		})

		t.Run(tt.name+"/empty", func(t *testing.T) {
			t.Setenv("AGENTTEAM_WORKSPACE_GROUP_ID", "")
			groupID, err := tt.parse(nil)
			if tt.requiredErr != "" {
				require.EqualError(t, err, tt.requiredErr)
				return
			}
			require.NoError(t, err)
			require.Equal(t, tt.defaultGroupID, groupID)
		})

		t.Run(tt.name+"/unexpected_args", func(t *testing.T) {
			_, err := tt.parse([]string{"extra"})
			require.EqualError(t, err, tt.unexpectedArgsErr)
		})
	}
}

func TestParseWorkspacePlacementRepairArgs(t *testing.T) {
	t.Setenv("AGENTTEAM_WORKSPACE_GROUP_ID", " env-group ")

	target, err := parseWorkspacePlacementRepairArgs([]string{"--workspace-id", " wsp_1 "})
	require.NoError(t, err)
	require.Equal(t, workspacePlacementRepairTarget{
		workspaceID:      "wsp_1",
		workspaceGroupID: "env-group",
	}, target)

	target, err = parseWorkspacePlacementRepairArgs([]string{
		"--workspace-id", " wsp_2 ",
		"--workspace-group-id", " flag-group ",
	})
	require.NoError(t, err)
	require.Equal(t, workspacePlacementRepairTarget{
		workspaceID:      "wsp_2",
		workspaceGroupID: "flag-group",
	}, target)
}

func TestParseWorkspacePlacementRepairArgsRequiresExplicitTarget(t *testing.T) {
	t.Setenv("AGENTTEAM_WORKSPACE_GROUP_ID", "")

	_, err := parseWorkspacePlacementRepairArgs([]string{"--workspace-group-id", "workspace-large-a"})
	require.EqualError(t, err, "workspace-placement-repair requires --workspace-id")

	_, err = parseWorkspacePlacementRepairArgs([]string{"--workspace-id", "wsp_1"})
	require.EqualError(t, err, "workspace-placement-repair requires --workspace-group-id or AGENTTEAM_WORKSPACE_GROUP_ID")

	_, err = parseWorkspacePlacementRepairArgs([]string{
		"--workspace-id", "wsp_1",
		"--workspace-group-id", "workspace-large-a",
		"extra",
	})
	require.EqualError(t, err, "unexpected workspace-placement-repair arguments: extra")
}

func TestWriteWorkspacePlacementRepairOutputsJSON(t *testing.T) {
	repairer := &fakeWorkspacePlacementRepairer{
		result: workspace.WorkspacePlacement{
			WorkspaceID:      "wsp_1",
			WorkspaceGroupID: "workspace-large-a",
			State:            workspace.PlacementStateActive,
			PlacementEpoch:   1,
		},
	}
	var output bytes.Buffer

	err := writeWorkspacePlacementRepair(
		context.Background(),
		repairer,
		workspacePlacementRepairTarget{workspaceID: "wsp_1", workspaceGroupID: "workspace-large-a"},
		&output,
	)

	require.NoError(t, err)
	require.Equal(t, "wsp_1", repairer.workspaceID)
	require.Equal(t, "workspace-large-a", repairer.workspaceGroupID)
	require.JSONEq(t, `{
		"workspace_id": "wsp_1",
		"workspace_group_id": "workspace-large-a",
		"state": "active",
		"placement_epoch": 1
	}`, strings.TrimSpace(output.String()))
}

func TestUsageIncludesWorkspacePlacementRepair(t *testing.T) {
	output := captureStderr(t, printUsage)

	require.Contains(t, output, "workspace-placement-repair")
	require.Contains(t, output, "create one explicitly targeted missing workspace_placements row")
}

func TestWriteWorkspacePlacementVerificationOutputsJSON(t *testing.T) {
	verifier := &fakeWorkspacePlacementVerifier{
		result: workspace.WorkspacePlacementVerification{
			WorkspaceGroupID:         "workspace-default",
			TotalWorkspaceCount:      2,
			PlacementCount:           2,
			MissingPlacementCount:    0,
			NonDefaultPlacementCount: 0,
			InactivePlacementCount:   0,
		},
	}
	var output bytes.Buffer

	err := writeWorkspacePlacementVerification(context.Background(), verifier, " workspace-default ", &output)

	require.NoError(t, err)
	require.Equal(t, " workspace-default ", verifier.workspaceGroupID)
	require.JSONEq(t, `{
		"workspace_group_id": "workspace-default",
		"total_workspace_count": 2,
		"placement_count": 2,
		"missing_placement_count": 0,
		"non_default_placement_count": 0,
		"inactive_placement_count": 0
	}`, strings.TrimSpace(output.String()))
}

func TestWriteWorkspacePlacementVerificationFailsNonZeroCounts(t *testing.T) {
	verifier := &fakeWorkspacePlacementVerifier{
		result: workspace.WorkspacePlacementVerification{
			WorkspaceGroupID:         "workspace-default",
			TotalWorkspaceCount:      4,
			PlacementCount:           3,
			MissingPlacementCount:    1,
			NonDefaultPlacementCount: 2,
			InactivePlacementCount:   3,
			MalformedPlacementCount:  1,
		},
	}
	var output bytes.Buffer

	err := writeWorkspacePlacementVerification(context.Background(), verifier, "workspace-default", &output)

	require.Error(t, err)
	require.Contains(t, err.Error(), "workspace placement verify failed for group workspace-default")
	require.Contains(t, err.Error(), "missing_placement_count=1")
	require.Contains(t, err.Error(), "non_default_placement_count=2")
	require.Contains(t, err.Error(), "inactive_placement_count=3")
	require.Contains(t, err.Error(), "malformed_placement_count=1")
	require.JSONEq(t, `{
		"workspace_group_id": "workspace-default",
		"total_workspace_count": 4,
		"placement_count": 3,
		"missing_placement_count": 1,
		"non_default_placement_count": 2,
		"inactive_placement_count": 3,
		"malformed_placement_count": 1
	}`, strings.TrimSpace(output.String()))
}

func TestParseWorkspacePlacementTopologyVerifyArgsRejectsUnexpectedArgs(t *testing.T) {
	require.NoError(t, parseWorkspacePlacementTopologyVerifyArgs(nil))
	err := parseWorkspacePlacementTopologyVerifyArgs([]string{"extra"})
	require.EqualError(t, err, "unexpected workspace-placement-topology-verify arguments: extra")
}

func TestWriteWorkspacePlacementTopologyVerificationOutputsJSON(t *testing.T) {
	verifier := &fakeWorkspacePlacementTopologyVerifier{
		result: workspace.WorkspacePlacementTopologyVerification{
			TotalWorkspaceCount:       2,
			WorkspaceGroupCount:       2,
			ActiveWorkspaceGroupCount: 2,
			PlacementCount:            2,
		},
	}
	var output bytes.Buffer

	err := writeWorkspacePlacementTopologyVerification(context.Background(), verifier, &output)

	require.NoError(t, err)
	require.True(t, verifier.called)
	require.JSONEq(t, `{
		"total_workspace_count": 2,
		"workspace_group_count": 2,
		"active_workspace_group_count": 2,
		"placement_count": 2,
		"missing_placement_count": 0,
		"unknown_group_placement_count": 0,
		"inactive_group_placement_count": 0,
		"inactive_placement_count": 0,
		"malformed_placement_count": 0,
		"active_migration_count": 0
	}`, strings.TrimSpace(output.String()))
}

func TestWriteWorkspacePlacementTopologyVerificationFailsNonZeroCounts(t *testing.T) {
	verifier := &fakeWorkspacePlacementTopologyVerifier{
		result: workspace.WorkspacePlacementTopologyVerification{
			TotalWorkspaceCount:         6,
			WorkspaceGroupCount:         2,
			ActiveWorkspaceGroupCount:   1,
			PlacementCount:              5,
			MissingPlacementCount:       1,
			UnknownGroupPlacementCount:  2,
			InactiveGroupPlacementCount: 3,
			InactivePlacementCount:      4,
			MalformedPlacementCount:     5,
			ActiveMigrationCount:        6,
		},
	}
	var output bytes.Buffer

	err := writeWorkspacePlacementTopologyVerification(context.Background(), verifier, &output)

	require.Error(t, err)
	require.Contains(t, err.Error(), "workspace placement topology verify failed")
	require.Contains(t, err.Error(), "missing_placement_count=1")
	require.Contains(t, err.Error(), "unknown_group_placement_count=2")
	require.Contains(t, err.Error(), "inactive_group_placement_count=3")
	require.Contains(t, err.Error(), "inactive_placement_count=4")
	require.Contains(t, err.Error(), "malformed_placement_count=5")
	require.Contains(t, err.Error(), "active_migration_count=6")
	require.JSONEq(t, `{
		"total_workspace_count": 6,
		"workspace_group_count": 2,
		"active_workspace_group_count": 1,
		"placement_count": 5,
		"missing_placement_count": 1,
		"unknown_group_placement_count": 2,
		"inactive_group_placement_count": 3,
		"inactive_placement_count": 4,
		"malformed_placement_count": 5,
		"active_migration_count": 6
	}`, strings.TrimSpace(output.String()))
}

type fakeWorkspacePlacementVerifier struct {
	result           workspace.WorkspacePlacementVerification
	err              error
	workspaceGroupID string
}

func (verifier *fakeWorkspacePlacementVerifier) VerifyWorkspacePlacements(_ context.Context, workspaceGroupID string) (workspace.WorkspacePlacementVerification, error) {
	verifier.workspaceGroupID = workspaceGroupID
	if verifier.err != nil {
		return workspace.WorkspacePlacementVerification{}, verifier.err
	}
	return verifier.result, nil
}

type fakeWorkspacePlacementRepairer struct {
	result           workspace.WorkspacePlacement
	err              error
	workspaceID      string
	workspaceGroupID string
}

func (repairer *fakeWorkspacePlacementRepairer) RepairMissingWorkspacePlacement(_ context.Context, workspaceID string, workspaceGroupID string) (workspace.WorkspacePlacement, error) {
	repairer.workspaceID = workspaceID
	repairer.workspaceGroupID = workspaceGroupID
	if repairer.err != nil {
		return workspace.WorkspacePlacement{}, repairer.err
	}
	return repairer.result, nil
}

type fakeWorkspacePlacementTopologyVerifier struct {
	result workspace.WorkspacePlacementTopologyVerification
	err    error
	called bool
}

func (verifier *fakeWorkspacePlacementTopologyVerifier) VerifyWorkspacePlacementTopology(context.Context) (workspace.WorkspacePlacementTopologyVerification, error) {
	verifier.called = true
	if verifier.err != nil {
		return workspace.WorkspacePlacementTopologyVerification{}, verifier.err
	}
	return verifier.result, nil
}
