package webagent

import (
	"context"
	"encoding/json"
	"errors"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/coredb/testkit"
	"github.com/tidbcloud/AgentTeam/internal/service/agentapproval"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/agentops"
)

type orderedDeleteService struct {
	calls  []string
	failID string
}

type recordingOperationTerminalObserver struct {
	inputs []agentops.OperationTerminalInput
}

func (observer *recordingOperationTerminalObserver) ObserveOperationTerminal(
	_ context.Context,
	input agentops.OperationTerminalInput,
) error {
	observer.inputs = append(observer.inputs, input)
	return nil
}

func (service *orderedDeleteService) CreateAgent(context.Context, string, string, CreateAgentInput) (agent.AppAgent, error) {
	return agent.AppAgent{}, errors.New("unexpected create")
}

func (service *orderedDeleteService) UpdateAgent(context.Context, string, string, UpdateAgentInput) (agent.AppAgent, error) {
	return agent.AppAgent{}, errors.New("unexpected update")
}

func (service *orderedDeleteService) DeleteAgent(_ context.Context, _, _, agentID string) error {
	service.calls = append(service.calls, agentID)
	if agentID == service.failID {
		return errors.New("database password must not escape")
	}
	return nil
}

func TestOperationWorkerAndExecutorPersistOrderedPartialDelete(t *testing.T) {
	ctx := context.Background()
	dsn := testkit.StartInitializedTiDBWithSharedRuntime(t, testkit.TiDBTestRuntimeUnistore)
	sqlDB, err := testkit.OpenTiDBContext(ctx, dsn)
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, sqlDB.Close()) })
	db := coredb.WrapSQLDB(sqlDB)
	operationStore := agentops.New(db, agentops.Options{})

	payload := agentapproval.Payload{
		SchemaVersion:   agentapproval.PayloadSchemaVersion,
		ClientRequestID: "req-delete",
		Applicant:       agentapproval.Applicant{AgentID: "agt_applicant", MemberID: "mem_applicant"},
		Owner:           agentapproval.OwnerRef{UserID: "usr_owner", MemberID: "mem_owner"},
		Action:          agentapproval.ActionDelete,
		Delete:          &agentapproval.DeleteProposal{AgentIDs: []string{"agt_first", "agt_fails", "agt_last"}},
	}
	payloadJSON, err := json.Marshal(payload)
	require.NoError(t, err)
	items := []json.RawMessage{
		json.RawMessage(`{"agentId":"agt_first"}`),
		json.RawMessage(`{"agentId":"agt_fails"}`),
		json.RawMessage(`{"agentId":"agt_last"}`),
	}
	var operationID int64
	require.NoError(t, coredb.WithTx(ctx, db, func(tx coredb.Tx) error {
		var enqueueErr error
		operationID, enqueueErr = operationStore.InTx(tx).EnqueueOperationInTx(ctx, agentops.EnqueueInput{
			WorkspaceID: "wsp_1", ApplicantAgentID: "agt_applicant", OwnerUserID: "usr_owner",
			Action: agentapproval.ActionDelete, Payload: payloadJSON,
			ApprovalID: "appr_1", RequestID: "req-delete", Items: items,
		})
		return enqueueErr
	}))

	service := &orderedDeleteService{failID: "agt_fails"}
	observer := &recordingOperationTerminalObserver{}
	worker, err := agentops.NewOperationWorker(
		operationStore, NewOperationExecutor(service), observer, 0, nil,
	)
	require.NoError(t, err)
	worker.ProcessOnce(ctx)

	require.Equal(t, []string{"agt_first", "agt_fails", "agt_last"}, service.calls,
		"items execute in strict order and continue after a failure")
	require.Len(t, observer.inputs, 1)
	require.Equal(t, "partial", observer.inputs[0].TerminalStatus)
	var operationStatus string
	require.NoError(t, db.QueryRow(ctx,
		`SELECT status FROM agent_operations WHERE id = $1`, operationID,
	).Scan(&operationStatus))
	require.Equal(t, "partial", operationStatus)

	rows, err := db.Query(ctx, `
SELECT item_index, status, result, error
FROM agent_operation_items
WHERE operation_id = $1
ORDER BY item_index`, operationID)
	require.NoError(t, err)
	defer rows.Close()
	type persistedItem struct {
		index  int
		status string
		result []byte
		err    string
	}
	var persisted []persistedItem
	for rows.Next() {
		var item persistedItem
		require.NoError(t, rows.Scan(&item.index, &item.status, &item.result, &item.err))
		persisted = append(persisted, item)
	}
	require.NoError(t, rows.Err())
	require.Len(t, persisted, 3)
	require.Equal(t, 0, persisted[0].index)
	require.Equal(t, "applied", persisted[0].status)
	require.JSONEq(t, `{"agentId":"agt_first"}`, string(persisted[0].result))
	require.Empty(t, persisted[0].err)
	require.Equal(t, "failed", persisted[1].status)
	require.Empty(t, persisted[1].result)
	require.Equal(t, "agent operation delete failed", persisted[1].err)
	require.NotContains(t, persisted[1].err, "password")
	require.Equal(t, 2, persisted[2].index)
	require.Equal(t, "applied", persisted[2].status)
	require.JSONEq(t, `{"agentId":"agt_last"}`, string(persisted[2].result))
	require.Empty(t, persisted[2].err)
}
