package service

import (
	"context"
	"os"
	"testing"
	"time"

	"github.com/pingcap/agent-git-service/internal/crypto"
	"github.com/pingcap/agent-git-service/internal/db"
	"github.com/pingcap/agent-git-service/internal/embedding"
	"github.com/pingcap/agent-git-service/internal/gitstore"
	"github.com/pingcap/agent-git-service/internal/tenant"
	"github.com/pingcap/agent-git-service/internal/wikicatalog"
	"gorm.io/gorm"
)

func TestReplaceWikiV2SnapshotSkipsStaleEmptyCandidate(t *testing.T) {
	svc, database, ctx, cleanup := newWikiV2InternalTestService(t)
	defer cleanup()

	setupRepoForInternalTest(t, svc, database, ctx, "staleempty", "wiki-v2-empty")
	full := "staleempty/wiki-v2-empty"

	if _, err := svc.PutWikiPage(ctx, full, "home", "# Home\n", "seed home", ""); err != nil {
		t.Fatalf("PutWikiPage(home): %v", err)
	}
	first, err := svc.ReconcileWikiV2(ctx, full)
	if err != nil {
		t.Fatalf("ReconcileWikiV2: %v", err)
	}

	repo, err := svc.GetRepo(ctx, full)
	if err != nil {
		t.Fatalf("GetRepo: %v", err)
	}
	result, err := svc.replaceWikiV2Snapshot(ctx, wikiRepoFullName(full), repo.ID, "", nil, nil, nil, time.Now().UTC())
	if err != nil {
		t.Fatalf("replaceWikiV2Snapshot empty candidate: %v", err)
	}
	if result.Applied {
		t.Fatalf("Applied = true, want false for stale empty candidate: %+v", result)
	}
	if result.CurrentHeadSHA != first.IndexedCommitSHA {
		t.Fatalf("CurrentHeadSHA = %q, want %q", result.CurrentHeadSHA, first.IndexedCommitSHA)
	}
	if result.CurrentPageCount != 1 {
		t.Fatalf("CurrentPageCount = %d, want 1", result.CurrentPageCount)
	}

	assertWikiV2StateUnchanged(t, database, repo.ID, first.IndexedCommitSHA, 1)
}

func TestReplaceWikiV2SnapshotSkipsStaleNonEmptyCandidate(t *testing.T) {
	svc, database, ctx, cleanup := newWikiV2InternalTestService(t)
	defer cleanup()

	setupRepoForInternalTest(t, svc, database, ctx, "stalehead", "wiki-v2-head")
	full := "stalehead/wiki-v2-head"

	if _, err := svc.PutWikiPage(ctx, full, "home", "# Home\n", "seed home", ""); err != nil {
		t.Fatalf("PutWikiPage(home): %v", err)
	}
	first, err := svc.ReconcileWikiV2(ctx, full)
	if err != nil {
		t.Fatalf("ReconcileWikiV2 first: %v", err)
	}
	if _, err := svc.PutWikiPage(ctx, full, "guides/setup", "# Setup\n", "seed setup", ""); err != nil {
		t.Fatalf("PutWikiPage(guides/setup): %v", err)
	}

	repo, err := svc.GetRepo(ctx, full)
	if err != nil {
		t.Fatalf("GetRepo: %v", err)
	}
	staleRows := []db.WikiPageIndex{{
		RepositoryID:  repo.ID,
		Slug:          "home",
		HeadBlobSHA:   "1111111111111111111111111111111111111111",
		HeadCommitSHA: first.IndexedCommitSHA,
		Title:         "Home",
		Size:          len("# Home\n"),
		UpdatedAt:     time.Now().UTC(),
	}}
	result, err := svc.replaceWikiV2Snapshot(ctx, wikiRepoFullName(full), repo.ID, first.IndexedCommitSHA, staleRows, nil, nil, time.Now().UTC())
	if err != nil {
		t.Fatalf("replaceWikiV2Snapshot stale head: %v", err)
	}
	if result.Applied {
		t.Fatalf("Applied = true, want false for stale head candidate: %+v", result)
	}
	if result.CurrentHeadSHA == first.IndexedCommitSHA {
		t.Fatalf("CurrentHeadSHA did not advance: %+v", result)
	}
	if result.CurrentPageCount != 1 {
		t.Fatalf("CurrentPageCount = %d, want existing row count 1", result.CurrentPageCount)
	}

	assertWikiV2StateUnchanged(t, database, repo.ID, first.IndexedCommitSHA, 1)
}

func assertWikiV2StateUnchanged(t *testing.T, database *gorm.DB, repoID uint, expectedSHA string, expectedRows int64) {
	t.Helper()

	var state db.WikiIndexState
	if err := database.First(&state, "repository_id = ?", repoID).Error; err != nil {
		t.Fatalf("load state: %v", err)
	}
	if state.IndexedCommitSHA != expectedSHA {
		t.Fatalf("IndexedCommitSHA = %q, want %q", state.IndexedCommitSHA, expectedSHA)
	}

	var rowCount int64
	if err := database.Model(&db.WikiPageIndex{}).Where("repository_id = ?", repoID).Count(&rowCount).Error; err != nil {
		t.Fatalf("count rows: %v", err)
	}
	if rowCount != expectedRows {
		t.Fatalf("rowCount = %d, want %d", rowCount, expectedRows)
	}
}

func setupRepoForInternalTest(
	t *testing.T,
	svc *Service,
	database *gorm.DB,
	ctx context.Context,
	login string,
	repoName string,
) {
	t.Helper()
	if err := database.Create(&db.User{Login: login, Name: login, Type: db.TypeUser}).Error; err != nil {
		t.Fatalf("seed user: %v", err)
	}
	if _, err := svc.CreateRepo(ctx, CreateRepoInput{OwnerLogin: login, Name: repoName}); err != nil {
		t.Fatalf("CreateRepo: %v", err)
	}
}

func newWikiV2InternalTestService(t *testing.T) (*Service, *gorm.DB, context.Context, func()) {
	t.Helper()

	info := testTenantInfo(tenant.ID("wiki-v2-internal"))
	ctx := tenant.WithInfo(t.Context(), info)
	tmpDir, err := os.MkdirTemp("", "wiki-v2-internal-")
	if err != nil {
		t.Fatalf("MkdirTemp: %v", err)
	}
	gdb, dbCleanup := openMigratedServiceTestDB(t)
	if err := db.EnforceTenantProvenance(gdb, info); err != nil {
		t.Fatalf("enforce tenant provenance: %v", err)
	}
	sqlDB, err := gdb.DB()
	if err != nil {
		t.Fatalf("sql.DB: %v", err)
	}
	sqlDB.SetMaxOpenConns(1)
	sqlDB.SetMaxIdleConns(1)

	store, err := gitstore.NewForTenant(tmpDir, info.ID)
	if err != nil {
		t.Fatalf("gitstore.New: %v", err)
	}
	wikiBlob := wikicatalog.NewBlobStore(tmpDir)
	wikiCat := wikicatalog.New(gdb, wikiBlob)
	secretBox, err := crypto.NewSecretBox("")
	if err != nil {
		t.Fatalf("crypto.NewSecretBox: %v", err)
	}
	resources := &TenantResources{
		Info:        info,
		DB:          gdb,
		Git:         store,
		WikiCatalog: wikiCat,
		WikiBlob:    wikiBlob,
		SecretBox:   secretBox,
		Embedder:    embedding.NopEmbedder{},
	}
	var svc *Service
	wikiCat.DBFor = func(ctx context.Context) *gorm.DB {
		return svc.DBForTenantResources(ctx, resources)
	}
	wikiCat.OnChangeSetCommitted = func(ctx context.Context, repoID uint, result wikicatalog.ChangeSetResult) error {
		return svc.WikiCatalogPostCommit(ctx, repoID, result)
	}
	if err := resources.Validate(); err != nil {
		t.Fatalf("validate tenant resources: %v", err)
	}
	svc = mustNewTenantService(t, testTenantResourcesProvider{info.ID: resources})

	return svc, gdb.WithContext(ctx), ctx, func() {
		dbCleanup()
		_ = os.RemoveAll(tmpDir)
	}
}
