package schema

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

	mysql "github.com/go-sql-driver/mysql"
)

func TestIsExpectedSchemaInitRetryError(t *testing.T) {
	tests := []struct {
		name          string
		err           error
		wantTransient bool
		wantExpected  bool
	}{
		{
			name:          "tidb information schema changed code",
			err:           &mysql.MySQLError{Number: 8028, Message: "Information schema is changed during the execution of the statement [try again later]"},
			wantTransient: true,
			wantExpected:  true,
		},
		{
			name:          "wrapped tidb retry message",
			err:           fmt.Errorf("exec ddl: %w", &mysql.MySQLError{Number: 1105, Message: "Information schema is changed during the execution of the statement [try again later]"}),
			wantTransient: true,
			wantExpected:  true,
		},
		{
			name:          "duplicate column",
			err:           &mysql.MySQLError{Number: 1060, Message: "Duplicate column name 'worktree_name'"},
			wantTransient: false,
			wantExpected:  true,
		},
		{
			name:          "table already exists",
			err:           &mysql.MySQLError{Number: 1050, Message: "Table 'fs_nodes' already exists"},
			wantTransient: false,
			wantExpected:  true,
		},
		{
			name:          "ordinary ddl error",
			err:           &mysql.MySQLError{Number: 1146, Message: "Table 'missing' doesn't exist"},
			wantTransient: false,
			wantExpected:  false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := IsTransientSchemaError(tt.err); got != tt.wantTransient {
				t.Fatalf("IsTransientSchemaError() = %v, want %v", got, tt.wantTransient)
			}
			if got := IsExpectedSchemaInitRetryError(tt.err); got != tt.wantExpected {
				t.Fatalf("IsExpectedSchemaInitRetryError() = %v, want %v", got, tt.wantExpected)
			}
		})
	}
}

func TestExecSchemaStatementRetriesTransientInformationSchemaError(t *testing.T) {
	var attempts atomic.Int32
	transient := &mysql.MySQLError{Number: 8028, Message: "Information schema is changed during the execution of the statement [try again later]"}
	db := newTestRepairDB(t, func(string) testRepairQueryResult {
		return testRepairQueryResult{}
	}, func(string) error {
		if attempts.Add(1) <= 2 {
			return transient
		}
		return nil
	})
	if err := execSchemaStatement(context.Background(), db, `ALTER TABLE uploads ADD COLUMN mode INT UNSIGNED NOT NULL DEFAULT 420`, 1, 1); err != nil {
		t.Fatalf("execSchemaStatement: %v", err)
	}
	if got := attempts.Load(); got != 3 {
		t.Fatalf("attempts = %d, want 3", got)
	}
}

func TestExecSchemaStatementStopsWhenRetryWaitIsCanceled(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	transient := &mysql.MySQLError{Number: 8028, Message: "Information schema is changed during the execution of the statement [try again later]"}
	db := newTestRepairDB(t, func(string) testRepairQueryResult {
		return testRepairQueryResult{}
	}, func(string) error {
		cancel()
		return transient
	})
	err := execSchemaStatement(ctx, db, `ALTER TABLE uploads ADD COLUMN mode INT UNSIGNED NOT NULL DEFAULT 420`, 1, 1)
	if err == nil || !errors.Is(err, context.Canceled) {
		t.Fatalf("error = %v, want context.Canceled", err)
	}
}

func TestSchemaRetryDelayCaps(t *testing.T) {
	if got, want := schemaRetryDelay(1), 50*time.Millisecond; got != want {
		t.Fatalf("attempt 1 delay = %s, want %s", got, want)
	}
	if got, want := schemaRetryDelay(2), 100*time.Millisecond; got != want {
		t.Fatalf("attempt 2 delay = %s, want %s", got, want)
	}
	if got, want := schemaRetryDelay(8), schemaStatementRetryMax; got != want {
		t.Fatalf("attempt 8 delay = %s, want %s", got, want)
	}
}
