package meshxruntime

import (
	"bufio"
	"bytes"
	"context"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"sync"
	"testing"
	"time"

	gogit "github.com/go-git/go-git/v5"
	gitconfig "github.com/go-git/go-git/v5/config"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/object"
	agsserver "github.com/pingcap/agent-git-service/server"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/agsbridge"
	"github.com/tidbcloud/AgentTeam/internal/config"
	"github.com/tidbcloud/AgentTeam/internal/database"
	"github.com/tidbcloud/AgentTeam/internal/resource/server"
	kbservice "github.com/tidbcloud/AgentTeam/internal/service/kb"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
	"github.com/tidbcloud/AgentTeam/internal/store/kb"
	reposstore "github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
)

func TestRuntimeRequiresWorkspaceGroupBeforeStartingWorkspaceWorkers(t *testing.T) {
	runtime := &Runtime{server: nil}
	require.Error(t, runtime.StartWorkers(WorkerOwnership{RequireGroup: true}))
}

func TestRuntimeRequiresWorkspaceScopeForGroupFencedWorkers(t *testing.T) {
	runtime := &Runtime{server: &server.Server{}}
	require.EqualError(t, runtime.StartWorkers(WorkerOwnership{WorkspaceGroupID: "workspace-default", RequireGroup: true, StartGlobalWorkers: true}), "start embedded MeshX workers: workspace scope is required")
}

func TestRuntimeReadyRequiresHandlerAndWorkers(t *testing.T) {
	runtime := &Runtime{}
	require.Error(t, runtime.Ready(context.Background()))
}

func TestRuntimeHandlerIsNilWithoutServer(t *testing.T) {
	var runtime *Runtime
	require.Nil(t, runtime.Handler())
	require.Nil(t, runtime.WikiGenerationAuthority())
	require.Nil(t, (&Runtime{server: &server.Server{}}).WikiGenerationAuthority())
}

func TestProductionRuntimeWorkerMirrorsUpstreamIntoEmbeddedAGS(t *testing.T) {
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("git executable is required")
	}
	root := t.TempDir()
	upstreamRoot := filepath.Join(root, "upstream")
	expectedCommit := writeRuntimeMirrorFixture(t, filepath.Join(upstreamRoot, "example", "source.git"))
	var upstreamMu sync.Mutex
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		upstreamMu.Lock()
		defer upstreamMu.Unlock()
		serveRuntimeGitHTTPBackend(upstreamRoot, w, r)
	}))
	t.Cleanup(upstream.Close)

	agsDSN := "file:" + filepath.Join(root, "ags.db") + "?_busy_timeout=5000&_journal_mode=WAL&_txlock=immediate"
	meshxDSN := "file:" + filepath.Join(root, "meshx.db") + "?_busy_timeout=5000&_journal_mode=WAL&_txlock=immediate"
	require.NoError(t, agsserver.MigrateDatabase(agsDSN, 0))
	meshxDB, err := database.Open(database.Config{DSN: meshxDSN})
	require.NoError(t, err)
	require.NoError(t, database.Close(meshxDB))

	agsRepoDir := filepath.Join(root, "ags-repos")
	runtime, err := newRuntime(config.ResourceRuntimeConfig{
		HTTPAddress: ":0",
		Auth:        config.ResourceRuntimeAuthConfig{Enabled: true, Header: "Authorization"},
		Database:    config.ResourceDatabaseConfig{DSN: meshxDSN},
		AGS: config.AGSConfig{
			DatabaseDSN: agsDSN,
			GitRepoDir:  agsRepoDir,
			BaseURL:     "http://127.0.0.1:3002",
		},
		Automation: config.AutomationConfig{Roles: []string{config.AutomationRoleAPI}, ServerRunnerEnabled: false},
		KB: config.KBConfig{
			SourceMirrorWorkDir:           filepath.Join(root, "mirror-work"),
			PublicRepoCacheDir:            filepath.Join(root, "public-cache"),
			SourceMirrorWorkerInterval:    10 * time.Millisecond,
			SourceMirrorSchedulerInterval: time.Hour,
			SourceMirrorTimeout:           10 * time.Second,
		},
	})
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, runtime.Shutdown(context.Background())) })
	require.NotNil(t, runtime.WikiGenerationAuthority())
	require.NoError(t, runtime.StartWorkers(WorkerOwnership{StartGlobalWorkers: true}))
	require.NoError(t, runtime.Ready(context.Background()))

	target, err := (kbservice.AGSHTTPRepositoryStore{
		Handler:             runtime.Handler(),
		AuthHeader:          "Authorization",
		AuthorizationHeader: agsbridge.AuthorizationHeader(),
	}).EnsureRepository(context.Background(), repos.AGSRepositoryEnsureRequest{
		WorkspaceID:   "wsp_1",
		ResourceType:  reposstore.ResourceTypeKBSource,
		ResourceID:    "src_1",
		RepoRole:      reposstore.RepoRoleMirror,
		Owner:         "wsp_1",
		Name:          "source",
		Kind:          "mirror",
		SourceType:    "codebase",
		Visibility:    "private",
		UserVisible:   true,
		DefaultBranch: "main",
	})
	require.NoError(t, err)

	seedDB, err := database.OpenWithoutMigrate(database.Config{DSN: meshxDSN})
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, database.Close(seedDB)) })
	now := time.Now().UTC()
	knowledgeBaseStore := kb.New(seedDB)
	_, err = knowledgeBaseStore.CreateKnowledgeBase(context.Background(), kb.KnowledgeBase{ID: "kb_1", WorkspaceID: "wsp_1", Slug: "runtime-mirror", Name: "Runtime mirror", RetrievalPolicy: "{}", SourceCollectionState: "{}", CreatedBy: "owner", UpdatedBy: "owner", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = knowledgeBaseStore.CreateSource(context.Background(), kb.Source{ID: "src_1", WorkspaceID: "wsp_1", KnowledgeBaseID: "kb_1", SourceType: "codebase", Status: kbservice.KBSourceStatusReady, Name: "Source", DefaultRef: "main", SyncStatus: kbservice.KBSourceStatusReady, IndexStatus: kbservice.KBSourceStatusReady, Config: "{}", CreatedBy: "owner", UpdatedBy: "owner", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = knowledgeBaseStore.CreateSourceMirror(context.Background(), kb.SourceMirror{SourceID: "src_1", WorkspaceID: "wsp_1", KnowledgeBaseID: "kb_1", UpstreamType: kb.SourceMirrorUpstreamTypeGenericGit, RemoteURL: upstream.URL + "/example/source.git", DefaultRef: "main", Enabled: true, SyncStatus: kb.SourceMirrorSyncStatusIdle, CreatedBy: "owner", UpdatedBy: "owner", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = reposstore.New(seedDB).Upsert(context.Background(), reposstore.ResourceRepository{ID: "rr_mirror", WorkspaceID: "wsp_1", ResourceType: reposstore.ResourceTypeKBSource, ResourceID: "src_1", RepoRole: reposstore.RepoRoleMirror, OwnerKind: "workspace", RepoOwner: target.Owner, RepoName: target.Name, RepoFullName: target.FullName, AGSRepoID: target.ID, DefaultRef: "main", Visibility: "private", UserVisible: true, CreatedBy: "owner", UpdatedBy: "owner", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = knowledgeBaseStore.CreateJob(context.Background(), kb.Job{ID: "job_runtime_mirror", WorkspaceID: "wsp_1", KnowledgeBaseID: "kb_1", SourceID: "src_1", JobType: kbservice.KBJobTypeSyncSourceMirror, Status: kbservice.KBJobStatusQueued, Progress: `{"source_id":"src_1","ref":"main","status":"queued","trigger":"manual","requested_by":"owner"}`, CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)

	require.EventuallyWithT(t, func(collect *assert.CollectT) {
		job, loadErr := knowledgeBaseStore.GetJob(context.Background(), "wsp_1", "job_runtime_mirror")
		require.NoError(collect, loadErr)
		require.Equal(collect, kbservice.KBJobStatusSucceeded, job.Status, job.Error)
	}, 10*time.Second, 20*time.Millisecond)

	targetRepo, err := gogit.PlainOpen(filepath.Join(agsRepoDir, "wsp_1", "source.git"))
	require.NoError(t, err)
	ref, err := targetRepo.Reference(plumbing.NewBranchReferenceName("main"), true)
	require.NoError(t, err)
	require.Equal(t, expectedCommit, ref.Hash())
}

func writeRuntimeMirrorFixture(t *testing.T, barePath string) plumbing.Hash {
	t.Helper()
	worktreePath := filepath.Join(t.TempDir(), "worktree")
	repo, err := gogit.PlainInit(worktreePath, false)
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(filepath.Join(worktreePath, "README.md"), []byte("runtime mirror fixture\n"), 0o600))
	worktree, err := repo.Worktree()
	require.NoError(t, err)
	_, err = worktree.Add("README.md")
	require.NoError(t, err)
	commit, err := worktree.Commit("initial", &gogit.CommitOptions{Author: &object.Signature{Name: "AgentTeam Test", Email: "agentteam@example.test", When: time.Now().UTC()}})
	require.NoError(t, err)
	require.NoError(t, os.MkdirAll(filepath.Dir(barePath), 0o700))
	_, err = gogit.PlainInit(barePath, true)
	require.NoError(t, err)
	_, err = repo.CreateRemote(&gitconfig.RemoteConfig{Name: "origin", URLs: []string{barePath}})
	require.NoError(t, err)
	require.NoError(t, repo.Push(&gogit.PushOptions{RemoteName: "origin", RefSpecs: []gitconfig.RefSpec{`+refs/heads/master:refs/heads/main`}}))
	bare, err := gogit.PlainOpen(barePath)
	require.NoError(t, err)
	require.NoError(t, bare.Storer.SetReference(plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName("main"))))
	return commit
}

func serveRuntimeGitHTTPBackend(projectRoot string, w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "read git request", http.StatusInternalServerError)
		return
	}
	cmd := exec.Command("git", "http-backend")
	cmd.Env = append(os.Environ(),
		"GIT_PROJECT_ROOT="+projectRoot,
		"GIT_HTTP_EXPORT_ALL=1",
		"REQUEST_METHOD="+r.Method,
		"PATH_INFO="+r.URL.Path,
		"QUERY_STRING="+r.URL.RawQuery,
		"CONTENT_TYPE="+r.Header.Get("Content-Type"),
		fmt.Sprintf("CONTENT_LENGTH=%d", len(body)),
	)
	cmd.Stdin = bytes.NewReader(body)
	var stderr bytes.Buffer
	cmd.Stderr = &stderr
	out, err := cmd.Output()
	if err != nil {
		http.Error(w, "run git http backend: "+stderr.String(), http.StatusInternalServerError)
		return
	}

	reader := bufio.NewReader(bytes.NewReader(out))
	status := http.StatusOK
	for {
		line, readErr := reader.ReadString('\n')
		if readErr != nil {
			http.Error(w, "read git CGI response", http.StatusInternalServerError)
			return
		}
		line = strings.TrimRight(line, "\r\n")
		if line == "" {
			break
		}
		key, value, ok := strings.Cut(line, ":")
		if !ok {
			continue
		}
		value = strings.TrimSpace(value)
		if strings.EqualFold(key, "Status") {
			_, _ = fmt.Sscanf(value, "%d", &status)
			continue
		}
		w.Header().Add(key, value)
	}
	w.WriteHeader(status)
	_, _ = io.Copy(w, reader)
}
