package bootstrapimpl

import (
	"context"
	"database/sql/driver"
	"errors"
	"fmt"
	"regexp"
	"strings"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
)

func TestFlushCounterUpsertsBatchesRowsWithinTransaction(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	rows := make([]counterUpsertRow, autonumberSeedBatchSize+1)
	for i := range rows {
		rows[i] = counterUpsertRow{
			ID:        fmt.Sprintf("counter-%d", i),
			FieldID:   "field-1",
			ScopeKey:  fmt.Sprintf("scope-%d", i),
			NextValue: int64(i + 1),
		}
	}

	mock.ExpectBegin()
	expectCounterUpsertChunk(mock, rows[:autonumberSeedBatchSize])
	expectCounterUpsertChunk(mock, rows[autonumberSeedBatchSize:])
	mock.ExpectCommit()

	if err := flushCounterUpserts(context.Background(), db, rows); err != nil {
		t.Fatalf("flushCounterUpserts: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet sqlmock expectations: %v", err)
	}
}

func TestFlushCounterUpsertsRollsBackWhenLaterChunkFails(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	rows := make([]counterUpsertRow, autonumberSeedBatchSize+1)
	for i := range rows {
		rows[i] = counterUpsertRow{
			ID:        fmt.Sprintf("counter-%d", i),
			FieldID:   "field-1",
			ScopeKey:  fmt.Sprintf("scope-%d", i),
			NextValue: int64(i + 1),
		}
	}
	wantErr := errors.New("write failed")

	mock.ExpectBegin()
	expectCounterUpsertChunk(mock, rows[:autonumberSeedBatchSize])
	query, args := counterUpsertChunkExpectation(rows[autonumberSeedBatchSize:])
	mock.ExpectExec(regexp.QuoteMeta(query)).
		WithArgs(args...).
		WillReturnError(wantErr)
	mock.ExpectRollback()

	err = flushCounterUpserts(context.Background(), db, rows)
	if !errors.Is(err, wantErr) {
		t.Fatalf("flushCounterUpserts error = %v, want wrapped %v", err, wantErr)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet sqlmock expectations: %v", err)
	}
}

func expectCounterUpsertChunk(mock sqlmock.Sqlmock, rows []counterUpsertRow) {
	query, args := counterUpsertChunkExpectation(rows)
	mock.ExpectExec(regexp.QuoteMeta(query)).
		WithArgs(args...).
		WillReturnResult(sqlmock.NewResult(0, int64(len(rows))))
}

func counterUpsertChunkExpectation(rows []counterUpsertRow) (string, []driver.Value) {
	const valueTuple = "(?, ?, ?, ?, ?, ?)"
	query := "INSERT INTO `_System_AutoNumberCounter` " +
		"(`Id`, `FieldId`, `ScopeKey`, `NextValue`, `CreatedDate`, `LastModifiedDate`) VALUES " +
		strings.TrimSuffix(strings.Repeat(valueTuple+", ", len(rows)), ", ") +
		" ON DUPLICATE KEY UPDATE " +
		"`NextValue` = GREATEST(`NextValue`, VALUES(`NextValue`)), " +
		"`LastModifiedDate` = VALUES(`LastModifiedDate`)"
	args := make([]driver.Value, 0, len(rows)*6)
	for _, row := range rows {
		args = append(args,
			row.ID,
			row.FieldID,
			row.ScopeKey,
			row.NextValue,
			sqlmock.AnyArg(),
			sqlmock.AnyArg(),
		)
	}
	return query, args
}
