package githttp

import (
	"net/http"
	"net/http/httptest"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/pingcap/agent-git-service/internal/db"
	"github.com/pingcap/agent-git-service/internal/service"
)

func TestReceivePackHandlerWakesOutboxAndReleasesRepositoryBeforeBlockedReplay(t *testing.T) {
	database, store, svc, handler, repository := newPushOutboxFailureFixture(t, 105)
	if err := database.AutoMigrate(
		&db.User{}, &db.AgentBinding{}, &db.Webhook{}, &db.HookDelivery{}, &db.PullRequest{}, &db.Release{},
		&db.BusinessUserActivityDaily{}, &db.BusinessRepoActivityDaily{}, &db.BusinessFirstUserAction{}, &db.BusinessFirstRepoAction{},
	); err != nil {
		t.Fatalf("AutoMigrate handler lifecycle models: %v", err)
	}
	user := db.User{ID: 105, Login: "owner", Type: db.TypeUser}
	if err := database.Create(&user).Error; err != nil {
		t.Fatalf("create owner: %v", err)
	}
	if err := database.Model(&db.Repository{}).Where("id = ?", repository.ID).Update("owner_id", user.ID).Error; err != nil {
		t.Fatalf("set repository owner: %v", err)
	}
	repository.OwnerID = user.ID
	repositoryPath, err := store.GetRepoPath(t.Context(), repository.FullName)
	if err != nil {
		t.Fatalf("GetRepoPath: %v", err)
	}
	seedPushOutboxRepository(t, repositoryPath)

	blocked := newBlockingHandlerResponseWriter()
	router := chi.NewRouter()
	router.Use(func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
			next.ServeHTTP(writer, request.WithContext(service.ContextWithUser(request.Context(), user)))
		})
	})
	router.Get("/{owner}/{repo}.git/info/refs", handler.InfoRefs)
	router.Post("/{owner}/{repo}.git/git-receive-pack", func(writer http.ResponseWriter, request *http.Request) {
		blocked.ResponseWriter = writer
		handler.ReceivePack(blocked, request)
	})
	server := httptest.NewServer(router)
	t.Cleanup(server.Close)
	t.Cleanup(blocked.unblock)
	svc.BaseURL = server.URL

	local := filepath.Join(t.TempDir(), "local")
	runHandlerLifecycleGit(t, t.TempDir(), "clone", repositoryPath, local)
	runHandlerLifecycleGit(t, local, "config", "user.name", "Lifecycle Test")
	runHandlerLifecycleGit(t, local, "config", "user.email", "lifecycle@example.invalid")
	if err := os.WriteFile(filepath.Join(local, "blocked.txt"), []byte("blocked replay\n"), 0o600); err != nil {
		t.Fatalf("write pushed file: %v", err)
	}
	runHandlerLifecycleGit(t, local, "add", "blocked.txt")
	runHandlerLifecycleGit(t, local, "commit", "-m", "exercise blocked replay")
	remoteURL := server.URL + "/" + repository.FullName + ".git"
	runHandlerLifecycleGit(t, local, "remote", "set-url", "origin", remoteURL)
	pushDone := make(chan error, 1)
	go func() {
		command := exec.Command("git", "-c", "http.proxy=", "-c", "https.proxy=", "-C", local, "push", "origin", "main")
		command.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")
		output, err := command.CombinedOutput()
		if err != nil {
			err = &handlerLifecycleGitError{err: err, output: string(output)}
		}
		pushDone <- err
	}()
	select {
	case <-blocked.started:
	case err := <-pushDone:
		t.Fatalf("push returned before response writer blocked: %v", err)
	case <-time.After(10 * time.Second):
		t.Fatal("receive-pack response did not reach blocking writer")
	}

	deadline := time.Now().Add(5 * time.Second)
	for {
		var remaining int64
		if err := database.Model(&db.RepositoryPushOutbox{}).Where("repository_id = ?", repository.ID).Count(&remaining).Error; err != nil {
			t.Fatalf("count push outbox: %v", err)
		}
		if remaining == 0 {
			break
		}
		if time.Now().After(deadline) {
			t.Fatal("post-push outbox was not woken while response replay was blocked")
		}
		time.Sleep(10 * time.Millisecond)
	}
	repositoryRoot, err := store.RepoRoot(t.Context())
	if err != nil {
		t.Fatalf("RepoRoot: %v", err)
	}
	spools, err := filepath.Glob(filepath.Join(repositoryRoot, ".ags-receive-pack-*.response"))
	if err != nil || len(spools) != 1 {
		t.Fatalf("blocked replay response spools = %v, err = %v, want one live owner", spools, err)
	}
	blocked.unblock()
	select {
	case err := <-pushDone:
		if err != nil {
			t.Fatalf("git push after replay release: %v", err)
		}
	case <-time.After(10 * time.Second):
		t.Fatal("git push did not finish after replay release")
	}
	deadline = time.Now().Add(5 * time.Second)
	for {
		spools, err = filepath.Glob(filepath.Join(repositoryRoot, ".ags-receive-pack-*.response"))
		if err != nil {
			t.Fatalf("glob response spools: %v", err)
		}
		if len(spools) == 0 {
			break
		}
		if time.Now().After(deadline) {
			t.Fatalf("handler leaked response spools: %v", spools)
		}
		time.Sleep(10 * time.Millisecond)
	}
}

type blockingHandlerResponseWriter struct {
	http.ResponseWriter
	started  chan struct{}
	release  chan struct{}
	once     sync.Once
	released sync.Once
}

func newBlockingHandlerResponseWriter() *blockingHandlerResponseWriter {
	return &blockingHandlerResponseWriter{started: make(chan struct{}), release: make(chan struct{})}
}

func (writer *blockingHandlerResponseWriter) Write(body []byte) (int, error) {
	writer.once.Do(func() { close(writer.started) })
	<-writer.release
	return writer.ResponseWriter.Write(body)
}

func (writer *blockingHandlerResponseWriter) unblock() {
	writer.released.Do(func() { close(writer.release) })
}

type handlerLifecycleGitError struct {
	err    error
	output string
}

func (err *handlerLifecycleGitError) Error() string {
	return err.err.Error() + ": " + strings.TrimSpace(err.output)
}

func runHandlerLifecycleGit(t *testing.T, directory string, arguments ...string) {
	t.Helper()
	command := exec.Command("git", arguments...)
	command.Dir = directory
	command.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")
	if output, err := command.CombinedOutput(); err != nil {
		t.Fatalf("git %v: %v: %s", arguments, err, output)
	}
}
