package scale_operation

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"google.golang.org/protobuf/types/known/timestamppb"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type testDBProvider struct {
	db *gorm.DB
}

func (p testDBProvider) ReadDB(context.Context) *gorm.DB  { return p.db }
func (p testDBProvider) WriteDB(context.Context) *gorm.DB { return p.db }

func TestScaleOperationRepo_CreateLoadSave(t *testing.T) {
	operationRepo := NewScaleOperationRepo(newScaleOperationTestDB(t))
	ctx := context.Background()
	now := time.Now()

	operation := &entity.ScaleOperation{
		ScaleOperation: apiv1.ScaleOperation{
			SharedPoolId:     "100",
			ComponentId:      "tikv-1",
			ScaleOperationId: "op-1",
			ComponentType:    apiv1.ComponentType_TIKV,
			Status:           apiv1.ScaleOperationStatus_SCALE_OPERATION_STATUS_RUNNING,
			Cancelable:       true,
			OriginalCount:    6,
			TargetCount:      12,
			ReadyCount:       9,
			PendingCount:     3,
			ResultSummary:    "running",
		},
		OriginalQuotaJSON: `{"node_count":6}`,
		Evidence: []*entity.ScaleOperationEvidence{{
			Source:     "kubernetes_event",
			Reason:     "FailedScheduling",
			Message:    "pending worker nodes",
			ObservedAt: timestamppb.New(now),
		}},
	}

	require.NoError(t, operationRepo.Create(ctx, operation))

	loaded, err := operationRepo.LoadByID(ctx, "100", "op-1", false)
	require.NoError(t, err)
	require.Equal(t, apiv1.ScaleOperationStatus_SCALE_OPERATION_STATUS_RUNNING, loaded.GetStatus())
	require.Equal(t, apiv1.ComponentType_TIKV, loaded.GetComponentType())
	require.Equal(t, uint32(9), loaded.GetReadyCount())
	require.Equal(t, uint32(3), loaded.GetPendingCount())
	require.Len(t, loaded.Evidence, 1)
	require.Equal(t, `{"node_count":6}`, loaded.OriginalQuotaJSON)

	loaded.Cancelable = false
	loaded.Status = apiv1.ScaleOperationStatus_SCALE_OPERATION_STATUS_SUCCEEDED
	loaded.ReadyCount = 12
	loaded.PendingCount = 0
	loaded.CompletedAt = timestamppb.New(now.Add(time.Minute))
	require.NoError(t, operationRepo.Save(ctx, loaded))

	resaved, err := operationRepo.LoadByID(ctx, "100", "op-1", false)
	require.NoError(t, err)
	require.False(t, resaved.GetCancelable())
	require.Equal(t, apiv1.ScaleOperationStatus_SCALE_OPERATION_STATUS_SUCCEEDED, resaved.GetStatus())
	require.Equal(t, uint32(12), resaved.GetReadyCount())
	require.Equal(t, uint32(0), resaved.GetPendingCount())
	require.NotNil(t, resaved.GetCompletedAt())
	require.True(t, loaded.GetCreatedAt().AsTime().Equal(resaved.GetCreatedAt().AsTime()), "Save must not alter CreatedAt")
}

func TestScaleOperationRepo_ListNonTerminal(t *testing.T) {
	operationRepo := NewScaleOperationRepo(newScaleOperationTestDB(t))
	ctx := context.Background()
	oldCompletedAt := time.Now().Add(-48 * time.Hour)

	require.NoError(t, operationRepo.Create(ctx, &entity.ScaleOperation{
		ScaleOperation: apiv1.ScaleOperation{
			SharedPoolId:     "100",
			ComponentId:      "tikv-1",
			ScaleOperationId: "running",
			Status:           apiv1.ScaleOperationStatus_SCALE_OPERATION_STATUS_RUNNING,
		},
	}))
	require.NoError(t, operationRepo.Create(ctx, &entity.ScaleOperation{
		ScaleOperation: apiv1.ScaleOperation{
			SharedPoolId:     "100",
			ComponentId:      "tikv-1",
			ScaleOperationId: "canceled",
			Status:           apiv1.ScaleOperationStatus_SCALE_OPERATION_STATUS_CANCELED,
			CompletedAt:      timestamppb.New(oldCompletedAt),
		},
	}))

	active, err := operationRepo.ListNonTerminal(ctx, 10)
	require.NoError(t, err)
	require.Len(t, active, 1)
	require.Equal(t, "running", active[0].GetScaleOperationId())
}

func TestScaleOperationRepo_FindActiveByComponent(t *testing.T) {
	operationRepo := NewScaleOperationRepo(newScaleOperationTestDB(t))
	ctx := context.Background()

	require.NoError(t, operationRepo.Create(ctx, &entity.ScaleOperation{
		ScaleOperation: apiv1.ScaleOperation{
			SharedPoolId:     "100",
			ComponentId:      "tikv-1",
			ScaleOperationId: "op-1",
			Status:           apiv1.ScaleOperationStatus_SCALE_OPERATION_STATUS_RUNNING,
		},
	}))
	require.NoError(t, operationRepo.Create(ctx, &entity.ScaleOperation{
		ScaleOperation: apiv1.ScaleOperation{
			SharedPoolId:     "100",
			ComponentId:      "tikv-2",
			ScaleOperationId: "op-2",
			Status:           apiv1.ScaleOperationStatus_SCALE_OPERATION_STATUS_SUCCEEDED,
		},
	}))

	active, err := operationRepo.FindActiveByComponent(ctx, "100", "tikv-1")
	require.NoError(t, err)
	require.Len(t, active, 1)
	require.Equal(t, "op-1", active[0].GetScaleOperationId())

	inactive, err := operationRepo.FindActiveByComponent(ctx, "100", "tikv-2")
	require.NoError(t, err)
	require.Empty(t, inactive)
}

func newScaleOperationTestDB(t *testing.T) testDBProvider {
	t.Helper()
	gdb, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, gdb.Exec(`
		CREATE TABLE scale_operations (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			created_at DATETIME,
			updated_at DATETIME,
			deleted_at DATETIME,
			shared_pool_id TEXT NOT NULL,
			component_id TEXT NOT NULL DEFAULT '',
			scale_operation_id TEXT NOT NULL,
			component_type INTEGER NOT NULL DEFAULT 0,
			status TEXT NOT NULL,
			cancelable BOOLEAN NOT NULL DEFAULT false,
			original_count INTEGER NOT NULL DEFAULT 0,
			target_count INTEGER NOT NULL DEFAULT 0,
			ready_count INTEGER NOT NULL DEFAULT 0,
			pending_count INTEGER NOT NULL DEFAULT 0,
			result_summary TEXT,
			original_quota_json TEXT,
			evidence_json TEXT,
			completed_at DATETIME
		)
	`).Error)
	return testDBProvider{db: gdb}
}
