package dm_svc

import (
	"context"
	"testing"

	"github.com/tidbcloud/pkgv2/bizerr"
	v1beta1 "github.com/tidbcloud/serverless-service/idl/pbgen/proto/v1beta1"
	"github.com/tidbcloud/serverless-service/internal/config"
	"github.com/tidbcloud/serverless-service/internal/domain/constant"
	"github.com/tidbcloud/serverless-service/internal/domain/entity"
	"github.com/tidbcloud/serverless-service/internal/domain/facade/mock_facade"
	"github.com/tidbcloud/serverless-service/internal/domain/repo/mock_repo"
	"github.com/tidbcloud/serverless-service/internal/domain/vo"
	regionalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/regional/serverless/v1"
	"go.uber.org/mock/gomock"
)

const (
	testInitFailureReason = "fail to initialize checker: same table name in case-insensitive " +
		"[same target table `ceo`.`t_user_deviceId_association` vs `ceo`.`t_user_deviceid_association`]"
	testInitFailureWorkaround = "Please check `target-table` config in task configuration file."
)

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

	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	dmTaskRepo := mock_repo.NewMockDMTaskRepo(ctrl)
	mgmt := mock_facade.NewMockMgmtSvcFacade(ctrl)
	encryptor := mock_facade.NewMockEncryptorFacade(ctrl)

	precheckReq := &entity.PrecheckRequest{
		DmPrecheck: &regionalserverlessv1.DMPrecheck{
			Mode: regionalserverlessv1.DMPrecheck_TASK_MODE_INCREMENTAL,
			Sources: []*regionalserverlessv1.DMPrecheck_Source{
				{ConnProfile: &regionalserverlessv1.DMPrecheck_ConnProfile{Host: "127.0.0.1", Port: 3306}},
			},
			TargetDb: &regionalserverlessv1.DMPrecheck_TargetDatabase{Host: "target", Port: 4000},
		},
	}
	itemTypes := vo.GetPrecheckItemList(precheckReq)
	if len(itemTypes) < 2 {
		t.Fatalf("expected at least 2 precheck items, got %d", len(itemTypes))
	}

	precheck := &entity.DMPrecheck{
		ID:         "dmp-test",
		ClusterID:  "cluster-1",
		Request:    precheckReq,
		TotalItems: int32(len(itemTypes)),
	}

	initErr := bizerr.New(bizerr.ErrCodeUnknown, "%s",
		"precheck execution failed: [code=26001], Message: "+testInitFailureReason+
			", Workaround: "+testInitFailureWorkaround)

	itemByID := make(map[string]*entity.DMPrecheckItem)
	var finishedItems []entity.DMPrecheckItem

	mgmt.EXPECT().
		DMPrecheck(gomock.Any(), gomock.Any()).
		Times(1).
		Return(nil, initErr)

	dmTaskRepo.EXPECT().
		CreatePrecheckItem(gomock.Any(), gomock.Any()).
		Times(len(itemTypes)).
		DoAndReturn(func(_ context.Context, item *entity.DMPrecheckItem) error {
			itemByID[item.ID] = item
			return nil
		})

	dmTaskRepo.EXPECT().
		UpdatePrecheckItem(gomock.Any(), gomock.Any(), gomock.Any()).
		Times(len(itemTypes)).
		DoAndReturn(func(_ context.Context, id string, updates map[string]any) error {
			item, ok := itemByID[id]
			if !ok {
				t.Fatalf("unknown precheck item id %s", id)
			}
			stage, ok := updates["stage"].(constant.DMPrecheckStage)
			if !ok {
				t.Fatalf("unexpected stage type %T", updates["stage"])
			}
			results, ok := updates["results"].(entity.Results)
			if !ok {
				t.Fatalf("unexpected results type %T", updates["results"])
			}
			finishedItems = append(finishedItems, entity.DMPrecheckItem{
				ID:          item.ID,
				Description: item.Description,
				Type:        item.Type,
				Stage:       stage,
				Results:     results,
			})
			return nil
		})

	dmTaskRepo.EXPECT().
		UpdatePrecheck(gomock.Any(), precheck.ID, gomock.Any()).
		AnyTimes().
		Return(nil)

	ctx := context.WithValue(context.Background(), config.CtxWorkerNameKey, "dmPrecheckWorkerTest")
	worker := NewDMPrecheckWorker(dmTaskRepo, mgmt, encryptor)
	if err := worker.processPrecheck(ctx, precheck); err != nil {
		t.Fatalf("processPrecheck() error = %v", err)
	}

	if precheck.Stage != constant.DMPrecheckStageFinished {
		t.Fatalf("precheck stage = %s, want %s", precheck.Stage, constant.DMPrecheckStageFinished)
	}
	if len(finishedItems) != len(itemTypes) {
		t.Fatalf("finished items = %d, want %d", len(finishedItems), len(itemTypes))
	}

	skippedReason := buildSkippedDueToInitFailureReason(testInitFailureReason)
	for i, item := range finishedItems {
		if item.Stage != constant.DMPrecheckStageFinished {
			t.Fatalf("item[%d] stage = %s, want %s", i, item.Stage, constant.DMPrecheckStageFinished)
		}
		reason := precheckItemReason(&item)
		solution := precheckItemSolution(&item)
		if solution != testInitFailureWorkaround {
			t.Fatalf("item[%d] solution = %q, want %q", i, solution, testInitFailureWorkaround)
		}
		if i == 0 {
			if reason != testInitFailureReason {
				t.Fatalf("item[0] reason = %q, want %q", reason, testInitFailureReason)
			}
			continue
		}
		if reason != skippedReason {
			t.Fatalf("item[%d] reason = %q, want %q", i, reason, skippedReason)
		}
	}

	precheck.DMPrecheckItems = finishedItems
	resp := vo.DMPrecheckToV1beta1PbPrecheck(precheck)
	if resp.GetFailedCnt() != precheck.TotalItems {
		t.Fatalf("failed_cnt = %d, want %d", resp.GetFailedCnt(), precheck.TotalItems)
	}
	if resp.GetStatus() != v1beta1.MigrationPrecheck_FAILED {
		t.Fatalf("status = %v, want %v", resp.GetStatus(), v1beta1.MigrationPrecheck_FAILED)
	}
	if resp.GetFailedCnt()+resp.GetWarnCnt()+resp.GetSuccessCnt() != precheck.TotalItems {
		t.Fatalf(
			"count sum = %d, want %d",
			resp.GetFailedCnt()+resp.GetWarnCnt()+resp.GetSuccessCnt(),
			precheck.TotalItems,
		)
	}
}

func precheckItemReason(item *entity.DMPrecheckItem) string {
	if len(item.Results.Results) == 0 || len(item.Results.Results[0].Errors) == 0 {
		return ""
	}
	return item.Results.Results[0].Errors[0].ShortErr
}

func precheckItemSolution(item *entity.DMPrecheckItem) string {
	if len(item.Results.Results) == 0 {
		return ""
	}
	return item.Results.Results[0].Instruction
}
