package app

import (
	"context"
	"errors"
	"sync/atomic"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

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

var errPostCommitAppOnboardingRead = errors.New("post-commit App onboarding read is forbidden by test")

type postCommitReadFailDB struct {
	coredb.DB
	committed atomic.Bool
}

func (db *postCommitReadFailDB) QueryRow(ctx context.Context, query string, args ...any) coredb.Row {
	if db.committed.Load() {
		return staticErrorRow{err: errPostCommitAppOnboardingRead}
	}
	return db.DB.QueryRow(ctx, query, args...)
}

func (db *postCommitReadFailDB) BeginTx(ctx context.Context, options coredb.TxOptions) (coredb.Tx, error) {
	tx, err := db.DB.BeginTx(ctx, options)
	if err != nil {
		return nil, err
	}
	return &postCommitReadFailTx{Tx: tx, committed: &db.committed}, nil
}

type postCommitReadFailTx struct {
	coredb.Tx
	committed *atomic.Bool
}

func (tx *postCommitReadFailTx) Commit(ctx context.Context) error {
	if err := tx.Tx.Commit(ctx); err != nil {
		return err
	}
	tx.committed.Store(true)
	return nil
}

type staticErrorRow struct{ err error }

func (row staticErrorRow) Scan(...any) error { return row.err }

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	admin := seedAppAdminFixture(t, pool, "member_bot_tx_response", time.Now().UTC())
	member := seedWorkspaceMemberFixture(t, pool, admin.WorkspaceID, "member_bot_tx_response_member", time.Now().UTC())
	failingDB := &postCommitReadFailDB{DB: pool}
	store := New(failingDB, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})

	result, err := store.RequestCustomAppInstallationForUser(
		ctx,
		member.UserID,
		admin.WorkspaceID,
		directAppOnboardingParams("member-app-transaction-response"),
	)
	require.NoError(t, err)
	require.True(t, failingDB.committed.Load())
	require.Equal(t, AppInstallationRequestStatusPending, result.Request.Status)
	require.NotEmpty(t, result.Request.ID)
	var requestCount int
	require.NoError(t, pool.QueryRow(ctx, `SELECT COUNT(*) FROM service_installation_requests WHERE id = $1`, result.Request.ID).Scan(&requestCount))
	require.Equal(t, 1, requestCount)
	var callbackSecret string
	require.NoError(t, pool.QueryRow(ctx, `SELECT COALESCE(JSON_UNQUOTE(JSON_EXTRACT(COALESCE(config, JSON_OBJECT()), '$.callbackSigningSecret')), '') FROM service_definitions WHERE id = $1`, result.Request.AppID).Scan(&callbackSecret))
	require.Empty(t, callbackSecret, "a pending member request must not create a callback secret")
}

func TestAppOnboardingWithSameSlugCreatesExactlyOneDefinition(t *testing.T) {
	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	admin := seedAppAdminFixture(t, pool, "bot_onboarding_unique", time.Now().UTC())
	first := seedWorkspaceMemberFixture(t, pool, admin.WorkspaceID, "bot_onboarding_unique_first", time.Now().UTC())
	second := seedWorkspaceMemberFixture(t, pool, admin.WorkspaceID, "bot_onboarding_unique_second", time.Now().UTC())
	params := directAppOnboardingParams("app-onboarding-unique")

	created, err := store.RequestCustomAppInstallationForUser(ctx, first.UserID, admin.WorkspaceID, params)
	require.NoError(t, err)
	require.NotEmpty(t, created.Request.ID)
	_, err = store.RequestCustomAppInstallationForUser(ctx, second.UserID, admin.WorkspaceID, params)
	require.ErrorIs(t, err, ErrConflict)
	require.ErrorContains(t, err, "already exists")
	require.Equal(t, 1, appInstallationRequestCountForWorkspace(t, ctx, pool, admin.WorkspaceID))
	var definitionCount int
	require.NoError(t, pool.QueryRow(ctx, `SELECT COUNT(*) FROM service_definitions WHERE slug = $1`, params.Slug).Scan(&definitionCount))
	require.Equal(t, 1, definitionCount)
}
