package service_test

import (
	"errors"
	"fmt"
	"strings"
	"testing"

	"github.com/pingcap/agent-git-service/internal/db"
	"github.com/pingcap/agent-git-service/internal/gitstore"
	"github.com/pingcap/agent-git-service/internal/service"
	"github.com/pingcap/agent-git-service/internal/wikicatalog"
)

func TestWikiPublicationReadsStayConsistentAfterInterruptedPublish(t *testing.T) {
	svc, handle, cleanup := setupTestService(t)
	defer cleanup()
	ctx := handle.Context()
	setupRepoForTest(t, svc, ctx, "diagnose", "prepared")
	full := "diagnose/prepared"
	oldBody, newBody := "# Home\n\nCalls: 1366\n", "# Home\n\nCalls: 1545\n"
	oldPage, err := svc.PutWikiPage(ctx, full, "home", oldBody, "initial", "")
	if err != nil {
		t.Fatal(err)
	}
	indexed, err := svc.ReconcileWikiV2(ctx, full)
	if err != nil {
		t.Fatal(err)
	}
	failed := errors.New("diagnostic: interrupted ref publication")
	service.SetTestWikiPreparedPublishFailureForTest(svc, func(repo, commit string) error {
		if repo == full {
			return failed
		}
		return nil
	})
	defer service.SetTestWikiPreparedPublishFailureForTest(svc, nil)
	if _, err = svc.PutWikiPage(ctx, full, "home", newBody, "new corpus", oldPage.SHA); !errors.Is(err, failed) {
		t.Fatalf("injected publication failure: %v", err)
	}
	service.SetTestWikiPreparedPublishFailureForTest(svc, nil)

	page, err := svc.GetWikiPage(ctx, full, "home")
	if err != nil {
		t.Fatal(err)
	}
	pages, err := svc.ListWikiPages(ctx, full, service.ListWikiPagesOptions{Recursive: true})
	if err != nil {
		t.Fatal(err)
	}
	tree, err := svc.ListWikiTreeAtRef(ctx, full, "", "")
	if err != nil {
		t.Fatal(err)
	}
	if len(pages) != 1 || len(tree) != 1 || page.Body != newBody || pages[0].SHA != page.SHA || tree[0].SHA != page.SHA {
		t.Fatalf("inconsistent views: page=%+v list=%+v tree=%+v", page, pages, tree)
	}

	reconciled, err := svc.ReconcileWikiV2(ctx, full)
	if err != nil {
		t.Fatal(err)
	}
	after, err := svc.GetWikiPage(ctx, full, "home")
	if err != nil {
		t.Fatal(err)
	}
	if !reconciled.Reconciled || reconciled.IndexedCommitSHA == indexed.IndexedCommitSHA || after.Body != newBody {
		t.Fatalf("unexpected reconcile result: %+v page=%+v", reconciled, after)
	}

	recovered, err := svc.PutWikiPage(ctx, full, "home", newBody, "guarded recovery", tree[0].SHA)
	if err != nil {
		t.Fatalf("ordinary prepared projection should recover: %v", err)
	}
	after, err = svc.GetWikiPage(ctx, full, "home")
	if err != nil || after.Body != newBody || recovered.SHA != after.SHA {
		t.Fatalf("recovered read = %+v, error=%v", after, err)
	}
}

func TestWikiPublicationLifecycleWithLegacyBacklog(t *testing.T) {
	svc, handle, cleanup := setupTestService(t)
	defer cleanup()
	ctx := handle.Context()
	setupRepoForTest(t, svc, ctx, "lifecycle", "wiki")
	full := "lifecycle/wiki"
	if _, err := svc.PutWikiPage(ctx, full, "old", "old body", "seed", ""); err != nil {
		t.Fatal(err)
	}
	if _, err := svc.ReconcileWikiV2(ctx, full); err != nil {
		t.Fatal(err)
	}
	repo, err := svc.GetRepo(ctx, full)
	if err != nil {
		t.Fatal(err)
	}
	catalog := handle.WikiCatalog()
	hook := catalog.OnChangeSetCommitted
	catalog.OnChangeSetCommitted = nil
	defer func() { catalog.OnChangeSetCommitted = hook }()
	large := strings.Repeat("revision body ", wikicatalog.MaxBodyInlineBytes)
	steps := []wikicatalog.Change{
		{Op: wikicatalog.OpUpsert, Slug: "new", Body: []byte(large)},
		{Op: wikicatalog.OpRename, Slug: "old", NewSlug: "renamed"},
		{Op: wikicatalog.OpUpsert, Slug: "new", Body: []byte{}},
		{Op: wikicatalog.OpDelete, Slug: "renamed"},
	}
	for _, change := range steps {
		if _, err := catalog.ApplyChangeSet(ctx, wikicatalog.ChangeSetRequest{RepositoryID: repo.ID, Source: wikicatalog.SourceBatch, Changes: []wikicatalog.Change{change}}); err != nil {
			t.Fatal(err)
		}
	}
	for _, slug := range []string{"old", "renamed"} {
		if _, err := svc.GetWikiPage(ctx, full, slug); !errors.Is(err, service.ErrNotFound) {
			t.Fatalf("deleted %s resurrected: %v", slug, err)
		}
	}
	listed, total, err := svc.ListWikiPagesPaginated(ctx, full, service.ListWikiPagesOptions{Recursive: true}, 0, 1)
	if err != nil || total != 1 || len(listed) != 1 || listed[0].Slug != "new" {
		t.Fatalf("list=%+v total=%d err=%v", listed, total, err)
	}
	page, err := svc.GetWikiPage(ctx, full, "new")
	if err != nil || page.Body != "" || page.SHA != listed[0].SHA {
		t.Fatalf("empty page=%+v %v", page, err)
	}
	catalog.OnChangeSetCommitted = hook
	if _, err := svc.ReconcileWikiV2(ctx, full); err != nil {
		t.Fatal(err)
	}
	paths, err := handle.Git().ListTreeFilesAtRef(ctx, full+".wiki", "master")
	if err != nil || len(paths) != 1 || paths[0] != "new.md" {
		t.Fatalf("Git lifecycle state=%v %v", paths, err)
	}
	var versions []db.WikiChangeset
	if err := handle.DB().Where("repository_id = ? AND source = ?", repo.ID, "batch").Order("changeset_id ASC").Find(&versions).Error; err != nil {
		t.Fatal(err)
	}
	if len(versions) != len(steps) {
		t.Fatalf("versions=%+v", versions)
	}
	body, err := handle.Git().ReadFileAtRef(ctx, full+".wiki", "new.md", versions[0].SynthCommitSHA)
	if err != nil || string(body) != large {
		t.Fatalf("large historical body lost: size=%d err=%v", len(body), err)
	}
}

func TestWikiPublicationLegacyRecoveryPreservesDivergentGit(t *testing.T) {
	svc, handle, cleanup := setupTestService(t)
	defer cleanup()
	ctx := handle.Context()
	setupRepoForTest(t, svc, ctx, "divergent", "wiki")
	full := "divergent/wiki"
	if _, err := svc.PutWikiPage(ctx, full, "home", "base", "seed", ""); err != nil {
		t.Fatal(err)
	}
	repo, err := svc.GetRepo(ctx, full)
	if err != nil {
		t.Fatal(err)
	}
	catalog := handle.WikiCatalog()
	hook := catalog.OnChangeSetCommitted
	catalog.OnChangeSetCommitted = nil
	defer func() { catalog.OnChangeSetCommitted = hook }()
	if _, err := catalog.ApplyChangeSet(ctx, wikicatalog.ChangeSetRequest{RepositoryID: repo.ID, Source: wikicatalog.SourceREST, Changes: []wikicatalog.Change{{Op: wikicatalog.OpUpsert, Slug: "home", Body: []byte("catalog")}}}); err != nil {
		t.Fatal(err)
	}
	gitSHA, err := handle.Git().CommitFiles(ctx, full+".wiki", "master", "external change", []gitstore.FileMutation{{Path: "home.md", Content: []byte("external")}})
	if err != nil {
		t.Fatal(err)
	}
	if err := svc.ReconcileWikiProjection(ctx, full); !errors.Is(err, service.ErrConflict) {
		t.Fatalf("expected explicit conflict, got %v", err)
	}
	head, err := handle.Git().HeadSHA(ctx, full+".wiki", "master")
	if err != nil || head != gitSHA {
		t.Fatalf("overwrote divergent Git: %s %v", head, err)
	}
	page, err := svc.GetWikiPage(ctx, full, "home")
	if err != nil || page.Body != "catalog" {
		t.Fatalf("lost catalog content: %+v %v", page, err)
	}
}

func TestWikiPublicationRecoversLegacyBacklog(t *testing.T) {
	for _, pending := range []int{1, 2} {
		t.Run(fmt.Sprintf("pending-%d", pending), func(t *testing.T) {
			svc, handle, cleanup := setupTestService(t)
			defer cleanup()
			ctx := handle.Context()
			setupRepoForTest(t, svc, ctx, "diagnose", "legacy")
			full := "diagnose/legacy"
			if _, err := svc.PutWikiPage(ctx, full, "home", "# Initial\n", "initial", ""); err != nil {
				t.Fatal(err)
			}
			if _, err := svc.ReconcileWikiV2(ctx, full); err != nil {
				t.Fatal(err)
			}
			repo, err := svc.GetRepo(ctx, full)
			if err != nil {
				t.Fatal(err)
			}
			catalog := handle.WikiCatalog()
			hook := catalog.OnChangeSetCommitted
			catalog.OnChangeSetCommitted = nil
			defer func() { catalog.OnChangeSetCommitted = hook }()
			for i := range pending {
				_, err := catalog.ApplyChangeSet(ctx, wikicatalog.ChangeSetRequest{
					RepositoryID: repo.ID, Source: wikicatalog.SourceREST, Message: "legacy unprojected write",
					Changes: []wikicatalog.Change{{Op: wikicatalog.OpUpsert, Slug: "home", Body: []byte(fmt.Sprintf("# Pending %d\n", i))}},
				})
				if err != nil {
					t.Fatal(err)
				}
			}
			catalog.OnChangeSetCommitted = hook
			var current db.WikiPage
			if err := handle.DB().Where("repository_id = ? AND slug = ?", repo.ID, "home").First(&current).Error; err != nil {
				t.Fatal(err)
			}
			body := fmt.Sprintf("# Pending %d\n", pending-1)
			_, writeErr := svc.PutWikiPage(ctx, full, "home", body, "guarded recovery", current.HeadBlobSHA)
			if writeErr != nil {
				t.Fatalf("legacy backlog recovery: %v", writeErr)
			}
			var changesets []db.WikiChangeset
			if err := handle.DB().Where("repository_id = ? AND message = ?", repo.ID, "legacy unprojected write").Order("changeset_id ASC").Find(&changesets).Error; err != nil {
				t.Fatal(err)
			}
			if len(changesets) != pending {
				t.Fatalf("lost history: %+v", changesets)
			}
			for i, change := range changesets {
				atRef, err := svc.GetWikiPageAtRef(ctx, full, "home", change.SynthCommitSHA)
				if err != nil || atRef.Body != fmt.Sprintf("# Pending %d\n", i) || change.SynthFormatVer != 1 {
					t.Fatalf("revision %d corrupted: %+v, %v", i, atRef, err)
				}
				gitBody, err := handle.Git().ReadFileAtRef(ctx, full+".wiki", "home.md", change.SynthCommitSHA)
				if err != nil || string(gitBody) != atRef.Body {
					t.Fatalf("Git revision %d body=%q error=%v", i, gitBody, err)
				}
			}
			if _, err := svc.ReconcileWikiV2(ctx, full); err != nil {
				t.Fatal(err)
			}
			_, retryErr := svc.PutWikiPage(ctx, full, "home", body, "after reconcile", current.HeadBlobSHA)
			if retryErr != nil {
				t.Fatalf("write after reconcile: %v", retryErr)
			}
		})
	}
}

func TestWikiPublicationRecoversHistoricalMarkerWithoutRewinding(t *testing.T) {
	svc, handle, cleanup := setupTestService(t)
	defer cleanup()
	ctx := handle.Context()
	setupRepoForTest(t, svc, ctx, "diagnose", "hidden")
	full := "diagnose/hidden"
	first, err := svc.PutWikiPage(ctx, full, "home", "# Initial\n", "initial", "")
	if err != nil {
		t.Fatal(err)
	}
	repo, err := svc.GetRepo(ctx, full)
	if err != nil {
		t.Fatal(err)
	}
	var older db.WikiChangeset
	if err := handle.DB().Where("repository_id = ?", repo.ID).First(&older).Error; err != nil {
		t.Fatal(err)
	}
	latest, err := svc.PutWikiPage(ctx, full, "home", "# Latest\n", "latest", first.SHA)
	if err != nil {
		t.Fatal(err)
	}
	if err := handle.DB().Model(&older).Update("synth_format_ver", 0).Error; err != nil {
		t.Fatal(err)
	}
	_, writeErr := svc.PutWikiPage(ctx, full, "home", latest.Body, "guarded recovery", latest.SHA)
	if writeErr != nil {
		t.Fatalf("historical marker recovery: %v", writeErr)
	}
	page, err := svc.GetWikiPage(ctx, full, "home")
	if err != nil || page.Body != latest.Body {
		t.Fatalf("live body regressed: %+v, %v", page, err)
	}
}

func TestWikiPublicationIgnoresCompactionSupersededPendingMarkers(t *testing.T) {
	svc, handle, cleanup := setupTestService(t)
	defer cleanup()
	ctx := handle.Context()
	setupRepoForTest(t, svc, ctx, "superseded", "wiki")
	full := "superseded/wiki"
	page, err := svc.PutWikiPage(ctx, full, "home", "first", "seed", "")
	if err != nil {
		t.Fatal(err)
	}
	page, err = svc.PutWikiPage(ctx, full, "home", "latest", "update", page.SHA)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := svc.CompactWikiHistory(ctx, full); err != nil {
		t.Fatal(err)
	}
	repo, err := svc.GetRepo(ctx, full)
	if err != nil {
		t.Fatal(err)
	}
	if err := handle.DB().Model(&db.WikiChangeset{}).Where("repository_id = ? AND superseded_by_changeset_id IS NOT NULL", repo.ID).UpdateColumn("synth_format_ver", 0).Error; err != nil {
		t.Fatal(err)
	}
	if err := svc.ReconcileWikiProjection(ctx, full); err != nil {
		t.Fatal(err)
	}
	if _, err := svc.PutWikiPage(ctx, full, "home", "after compact", "new update", page.SHA); err != nil {
		t.Fatal(err)
	}
	got, err := svc.GetWikiPage(ctx, full, "home")
	if err != nil || got.Body != "after compact" {
		t.Fatalf("page=%+v err=%v", got, err)
	}
}

func TestWikiPublicationLegacyBacklogAfterCompaction(t *testing.T) {
	svc, handle, cleanup := setupTestService(t)
	defer cleanup()
	ctx := handle.Context()
	setupRepoForTest(t, svc, ctx, "compact-base", "wiki")
	full := "compact-base/wiki"
	if _, err := svc.PutWikiPage(ctx, full, "home", "base", "seed", ""); err != nil {
		t.Fatal(err)
	}
	// Consecutive compactions exercise the catalog-parent/content-parent split.
	for range 2 {
		if _, err := svc.CompactWikiHistory(ctx, full); err != nil {
			t.Fatal(err)
		}
	}
	repo, err := svc.GetRepo(ctx, full)
	if err != nil {
		t.Fatal(err)
	}
	catalog := handle.WikiCatalog()
	hook := catalog.OnChangeSetCommitted
	catalog.OnChangeSetCommitted = nil
	defer func() { catalog.OnChangeSetCommitted = hook }()
	for _, body := range []string{"first after compact", "latest after compact"} {
		if _, err := catalog.ApplyChangeSet(ctx, wikicatalog.ChangeSetRequest{RepositoryID: repo.ID, Source: wikicatalog.SourceBatch, Changes: []wikicatalog.Change{{Op: wikicatalog.OpUpsert, Slug: "home", Body: []byte(body)}}}); err != nil {
			t.Fatal(err)
		}
	}
	catalog.OnChangeSetCommitted = hook
	if _, err := svc.ReconcileWikiV2(ctx, full); err != nil {
		t.Fatal(err)
	}
	body, err := handle.Git().ReadFileAtRef(ctx, full+".wiki", "home.md", "master")
	if err != nil || string(body) != "latest after compact" {
		t.Fatalf("Git body=%q err=%v", body, err)
	}
}
