package repositorylocalproxy

import (
	"bufio"
	"bytes"
	"context"
	"encoding/base64"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
	"time"
)

type auditSink struct{ events []AuditEvent }

func (a *auditSink) RecordRepositoryAccessProxyEvent(_ context.Context, e AuditEvent) {
	a.events = append(a.events, e)
}

type fakeResolver struct {
	token   string
	revoked bool
	expired bool
	calls   []string
}

func (f *fakeResolver) ResolveRepositoryCredential(_ context.Context, _ Lease, host, repo, capability string) (UpstreamCredential, error) {
	f.calls = append(f.calls, host+"/"+repo+":"+capability)
	return UpstreamCredential{Token: f.token, Revoked: f.revoked, Expired: f.expired}, nil
}

func newTestLease(t *testing.T, store *LeaseStore, caps []string, repos []string, now time.Time) Lease {
	t.Helper()
	store.tokenBytes = func() ([]byte, error) { return []byte("0123456789abcdef0123456789abcdef"), nil }
	lease, err := store.Issue(LeaseRequest{MachineID: "m1", AgentID: "agt1", SessionID: "sess1", WorkspaceID: "wsp1", Scope: RepositoryScope{ProviderHost: "github.com", Repositories: repos}, Capabilities: caps, AGSTokenID: "ags_secret_token", TTL: time.Hour, Now: now})
	if err != nil {
		t.Fatalf("Issue() error = %v", err)
	}
	return lease
}

func TestProxyAuthorizationMatrixForGitAndGhClone(t *testing.T) {
	now := time.Date(2026, 6, 9, 1, 0, 0, 0, time.UTC)
	cases := []struct {
		name      string
		path      string
		method    string
		caps      []string
		repos     []string
		mutate    func(*LeaseStore, Lease)
		resolver  fakeResolver
		want      int
		wantCalls int
	}{
		{name: "authorized git clone", path: "/_agentteam_git/github.com/tidbcloud/AgentTeam.git/info/refs?service=git-upload-pack", caps: []string{CapabilityRepoClone}, repos: []string{"pingcap/agentteam"}, resolver: fakeResolver{token: "ags_real_token"}, want: http.StatusOK, wantCalls: 1},
		{name: "authorized gh repo clone api", path: "/api/v3/repos/pingcap/agentteam", caps: []string{CapabilityRepoRead}, repos: []string{"pingcap/agentteam"}, resolver: fakeResolver{token: "ags_real_token"}, want: http.StatusOK, wantCalls: 1},
		{name: "no repo permission git", path: "/_agentteam_git/github.com/tidbcloud/AgentTeam.git/info/refs", caps: []string{CapabilityRepoClone}, repos: []string{"pingcap/other"}, resolver: fakeResolver{token: "ags_real_token"}, want: http.StatusForbidden, wantCalls: 0},
		{name: "no repo permission gh", path: "/api/v3/repos/pingcap/agentteam", caps: []string{CapabilityRepoRead}, repos: []string{"pingcap/other"}, resolver: fakeResolver{token: "ags_real_token"}, want: http.StatusForbidden, wantCalls: 0},
		{name: "wrong capability git clone", path: "/_agentteam_git/github.com/tidbcloud/AgentTeam.git/info/refs", caps: []string{CapabilityPullRequestRead}, repos: []string{"pingcap/agentteam"}, resolver: fakeResolver{token: "ags_real_token"}, want: http.StatusForbidden, wantCalls: 0},
		{name: "wrong capability gh pr create", path: "/api/v3/repos/pingcap/agentteam/pulls", method: http.MethodPost, caps: []string{CapabilityRepoRead}, repos: []string{"pingcap/agentteam"}, resolver: fakeResolver{token: "ags_real_token"}, want: http.StatusForbidden, wantCalls: 0},
		{name: "expired lease", path: "/_agentteam_git/github.com/tidbcloud/AgentTeam.git/info/refs", caps: []string{CapabilityRepoClone}, repos: []string{"pingcap/agentteam"}, mutate: func(_ *LeaseStore, _ Lease) { now = now.Add(2 * time.Hour) }, resolver: fakeResolver{token: "ags_real_token"}, want: http.StatusUnauthorized, wantCalls: 0},
		{name: "revoked lease", path: "/api/v3/repos/pingcap/agentteam", caps: []string{CapabilityRepoRead}, repos: []string{"pingcap/agentteam"}, mutate: func(s *LeaseStore, l Lease) { s.Revoke(l.ID, now.Add(time.Minute)) }, resolver: fakeResolver{token: "ags_real_token"}, want: http.StatusUnauthorized, wantCalls: 0},
		{name: "revoked ags token", path: "/_agentteam_git/github.com/tidbcloud/AgentTeam.git/info/refs", caps: []string{CapabilityRepoClone}, repos: []string{"pingcap/agentteam"}, resolver: fakeResolver{token: "ags_real_token", revoked: true}, want: http.StatusForbidden, wantCalls: 1},
		{name: "unknown mapping", path: "/api/v3/user", caps: []string{CapabilityRepoRead}, repos: []string{"pingcap/agentteam"}, resolver: fakeResolver{token: "ags_real_token"}, want: http.StatusBadRequest, wantCalls: 0},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			caseNow := time.Date(2026, 6, 9, 1, 0, 0, 0, time.UTC)
			now = caseNow
			store := NewLeaseStore()
			lease := newTestLease(t, store, tc.caps, tc.repos, caseNow)
			if tc.mutate != nil {
				tc.mutate(store, lease)
			}
			upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				if got := r.Header.Get("Authorization"); got != "Bearer ags_real_token" && !tc.resolver.revoked {
					t.Fatalf("upstream Authorization = %q", got)
				}
				_, _ = w.Write([]byte("ok"))
			}))
			defer upstream.Close()
			resolver := tc.resolver
			audit := &auditSink{}
			proxy := &Proxy{Store: store, Resolver: &resolver, UpstreamBaseURL: upstream.URL, Now: func() time.Time { return now }, Audit: audit}
			method := tc.method
			if method == "" {
				method = http.MethodGet
			}
			req := httptest.NewRequest(method, tc.path, nil)
			req.Header.Set("Authorization", "Bearer "+lease.Token)
			rec := httptest.NewRecorder()
			proxy.ServeHTTP(rec, req)
			if rec.Code != tc.want {
				t.Fatalf("status = %d body=%s want %d", rec.Code, rec.Body.String(), tc.want)
			}
			if len(resolver.calls) != tc.wantCalls {
				t.Fatalf("resolver calls = %d want %d (%v)", len(resolver.calls), tc.wantCalls, resolver.calls)
			}
			if tc.want >= 400 && len(audit.events) == 0 {
				t.Fatalf("denied request did not record audit event")
			}
			for _, event := range audit.events {
				if strings.Contains(event.Reason, lease.Token) || strings.Contains(event.Reason, "ags_real_token") {
					t.Fatalf("audit event leaked a token: %#v", event)
				}
			}
		})
	}
}

func TestRuntimeEnvForLeaseInjectsLocalhostOnlyAndRemovesBootstrapKey(t *testing.T) {
	token := "agentteam_lp_" + base64.RawURLEncoding.EncodeToString([]byte("local-only-token"))
	env, err := RuntimeEnvForLease(map[string]string{EnvLeaseToken: token, EnvProxyBase: "http://localhost:4567", EnvProviderHost: "github.com", "AGS_TOKEN": "must-not-copy"})
	if err != nil {
		t.Fatalf("RuntimeEnvForLease() error = %v", err)
	}
	if env["GH_HOST"] != "localhost" || env["GH_TOKEN"] != token || env["GH_ENTERPRISE_TOKEN"] != token {
		t.Fatalf("localhost GH env not injected: %#v", env)
	}
	if _, ok := env[EnvLeaseToken]; ok {
		t.Fatalf("bootstrap lease key leaked into runtime env")
	}
	if _, ok := env["AGS_TOKEN"]; ok {
		t.Fatalf("real AGS token env key leaked into runtime env")
	}
	if env["GIT_CONFIG_COUNT"] != "2" {
		t.Fatalf("git config count = %q want 2", env["GIT_CONFIG_COUNT"])
	}
	if !strings.Contains(env["GIT_CONFIG_KEY_0"], "insteadOf") || env["GIT_CONFIG_VALUE_0"] != "https://github.com/" {
		t.Fatalf("git clone rewrite missing: %#v", env)
	}
	if env["GIT_CONFIG_KEY_1"] != "http.http://localhost:4567/_agentteam_git/github.com/.extraHeader" || env["GIT_CONFIG_VALUE_1"] != "Authorization: Bearer "+token {
		t.Fatalf("git clone auth header missing: %#v", env)
	}
}

func TestRuntimeEnvForLeaseWithoutLeasePreservesExistingTokenEnv(t *testing.T) {
	input := map[string]string{
		"AGS_TOKEN":        "user-ags-token",
		"GITHUB_TOKEN":     "user-github-token",
		"GITEA_TOKEN":      "user-gitea-token",
		"GH_TOKEN":         "user-gh-token",
		EnvProxyBase:       "https://proxy.example.test",
		EnvProviderHost:    "github.com",
		"GIT_CONFIG_KEY_0": "existing-key",
	}
	env, err := RuntimeEnvForLease(input)
	if err != nil {
		t.Fatalf("RuntimeEnvForLease() without lease error = %v", err)
	}
	for key, want := range input {
		if env[key] != want {
			t.Fatalf("env[%s] = %q want %q; env=%#v", key, env[key], want, env)
		}
	}
	if _, ok := env["GH_ENTERPRISE_TOKEN"]; ok {
		t.Fatalf("local lease GH enterprise token injected without a lease: %#v", env)
	}
}

func TestRuntimeEnvForLeaseRejectsNonLocalhostProxyBase(t *testing.T) {
	token := "agentteam_lp_" + base64.RawURLEncoding.EncodeToString([]byte("local-only-token"))
	env, err := RuntimeEnvForLease(map[string]string{EnvLeaseToken: token, EnvProxyBase: "https://proxy.example.test", EnvProviderHost: "github.com", "AGS_TOKEN": "must-not-copy"})
	if err == nil {
		t.Fatalf("RuntimeEnvForLease() accepted non-localhost proxy base: %#v", env)
	}
	if _, ok := env[EnvLeaseToken]; ok {
		t.Fatalf("bootstrap lease key leaked after rejection")
	}
	if _, ok := env["AGS_TOKEN"]; ok {
		t.Fatalf("real AGS token env key leaked after rejection")
	}
}

func TestRedactionCoversLeaseAndAGSTokens(t *testing.T) {
	buf := &strings.Builder{}
	logger := slog.New(slog.NewTextHandler(buf, nil))
	store := NewLeaseStore()
	now := time.Date(2026, 6, 9, 1, 0, 0, 0, time.UTC)
	lease := newTestLease(t, store, []string{CapabilityRepoRead}, []string{"pingcap/agentteam"}, now)
	proxy := &Proxy{Store: store, Resolver: &fakeResolver{token: "ags_super_secret", revoked: true}, UpstreamBaseURL: "http://127.0.0.1:1", Now: func() time.Time { return now }, Logger: logger}
	req := httptest.NewRequest(http.MethodGet, "/api/v3/repos/pingcap/agentteam?token="+lease.Token, nil)
	req.Header.Set("Authorization", "Bearer "+lease.Token)
	rec := httptest.NewRecorder()
	proxy.ServeHTTP(rec, req)
	log := buf.String()
	if strings.Contains(log, lease.Token) || strings.Contains(log, "ags_super_secret") {
		t.Fatalf("log was not redacted: %s", log)
	}
	if Redact("x "+lease.Token+" y ags_super_secret") != "x [REDACTED] y [REDACTED]" {
		t.Fatalf("Redact did not redact known secret patterns")
	}
}

func TestRewriteCloneURLUsesLocalProxyPath(t *testing.T) {
	got, err := RewriteCloneURL("https://github.com/tidbcloud/AgentTeam.git", "http://localhost:4567")
	if err != nil {
		t.Fatalf("RewriteCloneURL() error = %v", err)
	}
	want := "http://localhost:4567/_agentteam_git/github.com/tidbcloud/AgentTeam.git"
	if got != want {
		t.Fatalf("RewriteCloneURL() = %q want %q", got, want)
	}
}

func TestProxyDisallowsQueryLeaseAndStripsSensitiveQueryUpstream(t *testing.T) {
	now := time.Date(2026, 6, 9, 1, 0, 0, 0, time.UTC)
	store := NewLeaseStore()
	lease := newTestLease(t, store, []string{CapabilityRepoClone}, []string{"pingcap/agentteam"}, now)

	upstreamQueries := []string{}
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		upstreamQueries = append(upstreamQueries, r.URL.RawQuery)
		_, _ = w.Write([]byte("ok"))
	}))
	defer upstream.Close()
	resolver := &fakeResolver{token: "ags_real_token"}
	proxy := &Proxy{Store: store, Resolver: resolver, UpstreamBaseURL: upstream.URL, Now: func() time.Time { return now }}

	req := httptest.NewRequest(http.MethodGet, "/_agentteam_git/github.com/tidbcloud/AgentTeam.git/info/refs?service=git-upload-pack&token="+lease.Token, nil)
	rec := httptest.NewRecorder()
	proxy.ServeHTTP(rec, req)
	if rec.Code != http.StatusUnauthorized {
		t.Fatalf("query lease auth status = %d body=%s want %d", rec.Code, rec.Body.String(), http.StatusUnauthorized)
	}
	if len(upstreamQueries) != 0 {
		t.Fatalf("query-auth request reached upstream: %v", upstreamQueries)
	}

	req = httptest.NewRequest(http.MethodGet, "/_agentteam_git/github.com/tidbcloud/AgentTeam.git/info/refs?service=git-upload-pack&token="+lease.Token+"&access_token=bad", nil)
	req.Header.Set("Authorization", "Bearer "+lease.Token)
	rec = httptest.NewRecorder()
	proxy.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("header-auth status = %d body=%s want 200", rec.Code, rec.Body.String())
	}
	if len(upstreamQueries) != 1 || upstreamQueries[0] != "service=git-upload-pack" {
		t.Fatalf("sensitive query was not stripped upstream: %#v", upstreamQueries)
	}
}

func TestLocalListenerSmokeForGhRESTAndGitCloneMatrix(t *testing.T) {
	gitPath, err := exec.LookPath("git")
	if err != nil {
		t.Skip("git binary is not available for high-fidelity clone smoke")
	}
	now := time.Date(2026, 6, 9, 1, 0, 0, 0, time.UTC)
	store := NewLeaseStore()
	lease := newTestLease(t, store, []string{CapabilityRepoRead, CapabilityRepoClone}, []string{"pingcap/agentteam"}, now)

	projectRoot := prepareBareHTTPRepo(t, gitPath)
	var upstreamSeen []string
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		upstreamSeen = append(upstreamSeen, r.Method+" "+r.URL.String()+" auth="+r.Header.Get("Authorization"))
		if strings.HasPrefix(r.URL.Path, "/api/v3/repos/pingcap/agentteam") {
			if r.URL.RawQuery != "" {
				t.Fatalf("REST proxy leaked unexpected raw query upstream: %q", r.URL.RawQuery)
			}
			w.Header().Set("Content-Type", "application/json")
			_, _ = w.Write([]byte(`{"full_name":"pingcap/agentteam"}`))
			return
		}
		serveGitHTTPBackend(t, projectRoot, w, r)
	}))
	defer upstream.Close()
	resolver := &fakeResolver{token: "ags_real_token"}
	proxy := &Proxy{Store: store, Resolver: resolver, UpstreamBaseURL: upstream.URL, Now: func() time.Time { return now }}
	listener := httptest.NewServer(proxy)
	defer listener.Close()

	client := listener.Client()
	req, err := http.NewRequest(http.MethodGet, listener.URL+"/api/v3/repos/pingcap/agentteam?token=must-strip", nil)
	if err != nil {
		t.Fatalf("build REST request: %v", err)
	}
	req.Header.Set("Authorization", "Bearer "+lease.Token)
	resp, err := client.Do(req)
	if err != nil {
		t.Fatalf("REST smoke request failed: %v", err)
	}
	_, _ = io.Copy(io.Discard, resp.Body)
	_ = resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("REST smoke status = %d want 200", resp.StatusCode)
	}

	env, err := RuntimeEnvForLease(map[string]string{EnvLeaseToken: lease.Token, EnvProxyBase: listener.URL, EnvProviderHost: "github.com"})
	if err != nil {
		t.Fatalf("RuntimeEnvForLease() error = %v", err)
	}
	dest := filepath.Join(t.TempDir(), "clone")
	cmd := exec.Command(gitPath, "clone", "https://github.com/tidbcloud/AgentTeam.git", dest)
	cmd.Env = mapToEnv(env, append(os.Environ(), "GIT_TERMINAL_PROMPT=0")...)
	out, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("git clone through local proxy failed: %v\n%s", err, out)
	}
	if _, err := os.Stat(filepath.Join(dest, "README.md")); err != nil {
		t.Fatalf("cloned repository missing README.md: %v", err)
	}
	joined := strings.Join(upstreamSeen, "\n")
	if !strings.Contains(joined, "/api/v3/repos/pingcap/agentteam auth=Bearer ags_real_token") {
		t.Fatalf("REST path did not reach upstream with resolver token:\n%s", joined)
	}
	if !strings.Contains(joined, "/pingcap/agentteam.git/info/refs?service=git-upload-pack auth=Bearer ags_real_token") {
		t.Fatalf("git clone path did not reach upstream smart HTTP with resolver token:\n%s", joined)
	}
}

func prepareBareHTTPRepo(t *testing.T, gitPath string) string {
	t.Helper()
	root := t.TempDir()
	work := filepath.Join(root, "work")
	runGit(t, gitPath, "", "init", work)
	if err := os.WriteFile(filepath.Join(work, "README.md"), []byte("hello local proxy\n"), 0o644); err != nil {
		t.Fatalf("write README: %v", err)
	}
	runGit(t, gitPath, work, "add", "README.md")
	runGit(t, gitPath, work, "-c", "user.name=AgentTeam Test", "-c", "user.email=agentteam@example.test", "commit", "-m", "seed")
	bareRoot := filepath.Join(root, "gitroot")
	if err := os.MkdirAll(filepath.Join(bareRoot, "pingcap"), 0o755); err != nil {
		t.Fatalf("mkdir bare root: %v", err)
	}
	runGit(t, gitPath, "", "clone", "--bare", work, filepath.Join(bareRoot, "pingcap", "agentteam.git"))
	runGit(t, gitPath, filepath.Join(bareRoot, "pingcap", "agentteam.git"), "update-server-info")
	return bareRoot
}

func runGit(t *testing.T, gitPath, dir string, args ...string) {
	t.Helper()
	cmd := exec.Command(gitPath, args...)
	if dir != "" {
		cmd.Dir = dir
	}
	cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")
	out, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("git %s failed: %v\n%s", strings.Join(args, " "), err, out)
	}
}

func serveGitHTTPBackend(t *testing.T, projectRoot string, w http.ResponseWriter, r *http.Request) {
	t.Helper()
	body, err := io.ReadAll(r.Body)
	if err != nil {
		t.Fatalf("read git request body: %v", err)
	}
	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 {
		t.Fatalf("git http-backend failed: %v\n%s", err, stderr.String())
	}
	writeCGIResponse(t, w, out)
}

func writeCGIResponse(t *testing.T, w http.ResponseWriter, out []byte) {
	t.Helper()
	reader := bufio.NewReader(bytes.NewReader(out))
	status := http.StatusOK
	for {
		line, err := reader.ReadString('\n')
		if err != nil {
			t.Fatalf("read CGI header: %v", err)
		}
		line = strings.TrimRight(line, "\r\n")
		if line == "" {
			break
		}
		key, val, ok := strings.Cut(line, ":")
		if !ok {
			continue
		}
		val = strings.TrimSpace(val)
		if strings.EqualFold(key, "Status") {
			fmt.Sscanf(val, "%d", &status)
			continue
		}
		w.Header().Add(key, val)
	}
	w.WriteHeader(status)
	_, _ = io.Copy(w, reader)
}

func mapToEnv(overrides map[string]string, base ...string) []string {
	merged := map[string]string{}
	for _, item := range base {
		k, v, ok := strings.Cut(item, "=")
		if ok {
			merged[k] = v
		}
	}
	for k, v := range overrides {
		merged[k] = v
	}
	out := make([]string, 0, len(merged))
	for k, v := range merged {
		out = append(out, k+"="+v)
	}
	return out
}
