package main

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func TestWriteSnapshotShardsSkipsEmptyPayloads(t *testing.T) {
	dir := t.TempDir()

	shards, err := writeSnapshotShards(dir, "issues", nil, 100, defaultMaxShardBytes)
	if err != nil {
		t.Fatalf("writeSnapshotShards: %v", err)
	}
	if len(shards) != 0 {
		t.Fatalf("expected no empty shards, got %+v", shards)
	}
	entries, err := os.ReadDir(dir)
	if err != nil {
		t.Fatalf("ReadDir: %v", err)
	}
	if len(entries) != 0 {
		t.Fatalf("expected no files for empty payload set, got %d", len(entries))
	}
}

func TestAGSCreateSendsTargetAuthorizationRequestWithoutSourceCredential(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/api/ext/v1/imports/github" {
			t.Fatalf("path = %q", r.URL.Path)
		}
		var body map[string]any
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			t.Fatalf("decode request: %v", err)
		}
		for _, key := range []string{"source_token", "source_token_ref", "start", "dry_run"} {
			if _, ok := body[key]; ok {
				t.Fatalf("create request must not send %s: %#v", key, body)
			}
		}
		if body["target_mode"] != "existing_empty" {
			t.Fatalf("target_mode = %#v", body["target_mode"])
		}
		if r.Header.Get("Authorization") != "Bearer target-token" {
			t.Fatalf("authorization = %q", r.Header.Get("Authorization"))
		}
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"id":123}`))
	}))
	defer server.Close()
	t.Setenv("AGS_TOKEN", "target-token")
	if err := agsCreate(context.Background(), []string{
		"--target", server.URL,
		"--source-repo", "source/repository",
		"--target-repo", "target/repository",
		"--target-mode", "existing_empty",
	}); err != nil {
		t.Fatalf("ags create: %v", err)
	}
}

func TestSnapshotAccessHandoffsRecordsSourceAccessWithoutDestinationGrants(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		switch r.URL.Path {
		case "/repos/source/repo/collaborators":
			_, _ = w.Write([]byte(`[{"id":101,"node_id":"U101","login":"alice","html_url":"https://github.test/alice","permissions":{"push":true}}]`))
		case "/repos/source/repo/teams":
			_, _ = w.Write([]byte(`[{"id":201,"node_id":"T201","name":"Maintainers","slug":"maintainers","html_url":"https://github.test/orgs/source/teams/maintainers","permission":"admin"}]`))
		case "/orgs/source/teams/maintainers/members":
			_, _ = w.Write([]byte(`[{"id":101,"node_id":"U101","login":"alice","html_url":"https://github.test/alice"}]`))
		default:
			t.Fatalf("unexpected access snapshot path %s", r.URL.Path)
		}
	}))
	defer server.Close()

	payloads, err := snapshotAccessHandoffs(context.Background(), newGitHubClient(server.URL, "source-token"), "source", "repo")
	if err != nil {
		t.Fatalf("snapshot access handoffs: %v", err)
	}
	if len(payloads) != 3 {
		t.Fatalf("handoff payload count = %d, want 3", len(payloads))
	}
	var kinds []string
	for _, payload := range payloads {
		var handoff snapshotAccessHandoff
		if err := json.Unmarshal(payload, &handoff); err != nil {
			t.Fatalf("decode handoff: %v", err)
		}
		kinds = append(kinds, handoff.Kind)
		if handoff.Kind == "collaborator" && (handoff.Permission != "write" || handoff.GitHubID != 101) {
			t.Fatalf("collaborator handoff = %+v", handoff)
		}
	}
	if strings.Join(kinds, ",") != "collaborator,team,team_member" {
		t.Fatalf("handoff kinds = %v", kinds)
	}
}

func TestSnapshotAccessHandoffsFailsWhenSourcePermissionsCannotBeRead(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/repos/source/repo/collaborators" {
			t.Fatalf("unexpected path %s", r.URL.Path)
		}
		http.Error(w, `{"message":"Not Found"}`, http.StatusNotFound)
	}))
	defer server.Close()

	_, err := snapshotAccessHandoffs(context.Background(), newGitHubClient(server.URL, "source-token"), "source", "repo")
	if err == nil || !strings.Contains(err.Error(), "read repository collaborators") {
		t.Fatalf("snapshot access handoffs error = %v, want collaborator read failure", err)
	}
}

func TestSnapshotReleaseAssetToFileRejectsIncompleteAssets(t *testing.T) {
	client := newGitHubClient("https://api.github.test", "")
	asset := json.RawMessage(`{"id":1,"name":"large.tar.gz","url":"https://api.github.test/assets/1","size":101}`)
	if _, _, err := snapshotReleaseAssetToFile(context.Background(), client, asset, 1, "v1.0.0", 100, t.TempDir()); err == nil || !strings.Contains(err.Error(), "exceeds") {
		t.Fatalf("expected oversized asset error, got %v", err)
	}
	missingURL := json.RawMessage(`{"id":2,"name":"missing.tar.gz","size":1}`)
	if _, _, err := snapshotReleaseAssetToFile(context.Background(), client, missingURL, 1, "v1.0.0", 100, t.TempDir()); err == nil || !strings.Contains(err.Error(), "no API URL") {
		t.Fatalf("expected missing asset URL error, got %v", err)
	}
}

func TestSnapshotReleaseAssetsWritesBinaryOutsideNDJSON(t *testing.T) {
	assetBytes := []byte("binary\x00asset")
	sum := sha256.Sum256(assetBytes)
	checksum := "sha256:" + hex.EncodeToString(sum[:])
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/releases/1/assets":
			_, _ = w.Write([]byte(`[{"id":900,"name":"asset.bin","size":12,"url":"http://` + r.Host + `/downloads/900"}]`))
		case "/downloads/900":
			_, _ = w.Write(assetBytes)
		default:
			t.Fatalf("unexpected path %s", r.URL.Path)
		}
	}))
	defer server.Close()

	dir := t.TempDir()
	release := json.RawMessage(`{"id":1,"tag_name":"v1.0.0","assets_url":"` + server.URL + `/releases/1/assets"}`)
	payloads, files, err := snapshotReleaseAssetsToFiles(context.Background(), newGitHubClient(server.URL, ""), []json.RawMessage{release}, 100, dir)
	if err != nil {
		t.Fatalf("snapshotReleaseAssetsToFiles: %v", err)
	}
	if len(payloads) != 1 || len(files) != 1 {
		t.Fatalf("payload/files = %d/%d, want 1/1", len(payloads), len(files))
	}
	if files[0].SourceID != "900" || files[0].Size != int64(len(assetBytes)) || files[0].Checksum != checksum {
		t.Fatalf("asset manifest = %+v", files[0])
	}
	path, err := snapshotAssetFilePath(dir, files[0].Path)
	if err != nil || verifySnapshotAssetFile(path, files[0].Size, files[0].Checksum) != nil {
		t.Fatalf("snapshot asset was not written and verified: path=%q err=%v", path, err)
	}
	if strings.Contains(string(payloads[0]), "content_base64") || !strings.Contains(string(payloads[0]), checksum) {
		t.Fatalf("release asset metadata = %s", payloads[0])
	}
}

func TestWriteSnapshotShardsSplitsByByteLimit(t *testing.T) {
	dir := t.TempDir()

	shards, err := writeSnapshotShards(dir, "issues", []json.RawMessage{
		json.RawMessage(`{"id":1,"body":"aaaa"}`),
		json.RawMessage(`{"id":2,"body":"bbbb"}`),
		json.RawMessage(`{"id":3,"body":"cccc"}`),
	}, 100, 24)
	if err != nil {
		t.Fatalf("writeSnapshotShards: %v", err)
	}
	if len(shards) != 3 {
		t.Fatalf("expected byte limit to split into 3 shards, got %d: %+v", len(shards), shards)
	}
	for _, shard := range shards {
		if shard.ByteSize <= 0 {
			t.Fatalf("expected shard byte size, got %+v", shard)
		}
	}
}

func TestSnapshotIssueCommentsUsesRepositoryEndpoint(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/repos/octo/repo/issues/comments" {
			t.Fatalf("unexpected path %s", r.URL.Path)
		}
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`[{"id":10,"node_id":"IC1","issue_url":"https://api.github.test/repos/octo/repo/issues/123","body":"hello"}]`))
	}))
	defer server.Close()

	payloads, err := snapshotIssueComments(context.Background(), newGitHubClient(server.URL, ""), "octo", "repo")
	if err != nil {
		t.Fatalf("snapshotIssueComments: %v", err)
	}
	if len(payloads) != 1 {
		t.Fatalf("payload count = %d, want 1", len(payloads))
	}
	if got := rawInt(payloads[0], "issue_number"); got != 123 {
		t.Fatalf("issue_number = %d, want 123", got)
	}
}

func TestSnapshotPullReviewCommentsUsesRepositoryEndpoint(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/repos/octo/repo/pulls/comments" {
			t.Fatalf("unexpected path %s", r.URL.Path)
		}
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`[{"id":20,"node_id":"RC1","pull_request_url":"https://api.github.test/repos/octo/repo/pulls/456","body":"line"}]`))
	}))
	defer server.Close()

	payloads, err := snapshotPullReviewComments(context.Background(), newGitHubClient(server.URL, ""), "octo", "repo", []json.RawMessage{
		json.RawMessage(`{"number":456}`),
	})
	if err != nil {
		t.Fatalf("snapshotPullReviewComments: %v", err)
	}
	if len(payloads) != 1 {
		t.Fatalf("payload count = %d, want 1", len(payloads))
	}
	if got := rawInt(payloads[0], "pull_request_number"); got != 456 {
		t.Fatalf("pull_request_number = %d, want 456", got)
	}
}

func TestSnapshotPullReviewCommentsSkipsUnknownPullRequests(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/repos/octo/repo/pulls/comments" {
			t.Fatalf("unexpected path %s", r.URL.Path)
		}
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`[
			{"id":20,"node_id":"RC1","pull_request_url":"https://api.github.test/repos/octo/repo/pulls/456","body":"line"},
			{"id":21,"node_id":"RC2","pull_request_url":"https://api.github.test/repos/octo/repo/pulls/999","body":"orphan"}
		]`))
	}))
	defer server.Close()

	payloads, err := snapshotPullReviewComments(context.Background(), newGitHubClient(server.URL, ""), "octo", "repo", []json.RawMessage{
		json.RawMessage(`{"number":456}`),
	})
	if err != nil {
		t.Fatalf("snapshotPullReviewComments: %v", err)
	}
	if len(payloads) != 1 {
		t.Fatalf("payload count = %d, want 1", len(payloads))
	}
	if got := rawInt(payloads[0], "pull_request_number"); got != 456 {
		t.Fatalf("pull_request_number = %d, want 456", got)
	}
}

func TestSnapshotPullRequestStacksUsesDocumentedEndpointAndSkipsDisabledCapability(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/repos/octo/repo/stacks":
			if got := r.URL.Query().Get("per_page"); got != "100" {
				t.Fatalf("per_page = %q, want 100", got)
			}
			if got := r.Header.Get("X-GitHub-Api-Version"); got != githubStacksAPIVersion {
				t.Fatalf("Stack API version = %q, want %q", got, githubStacksAPIVersion)
			}
			_, _ = w.Write([]byte(`[{"id":42,"number":7,"node_id":"S1","url":"https://api.github.test/repos/octo/repo/stacks/7","base":{"ref":"main"},"pull_requests":[{"number":10},{"number":11}]}]`))
		default:
			t.Fatalf("unexpected path %s", r.URL.Path)
		}
	}))
	defer server.Close()

	payloads, err := snapshotPullRequestStacks(context.Background(), newGitHubClient(server.URL, ""), "octo", "repo")
	if err != nil {
		t.Fatalf("snapshotPullRequestStacks: %v", err)
	}
	if len(payloads) != 1 || rawInt(payloads[0], "number") != 7 {
		t.Fatalf("Stack payloads = %s", payloads)
	}

	disabled := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.NotFound(w, r)
	}))
	defer disabled.Close()
	payloads, err = snapshotPullRequestStacks(context.Background(), newGitHubClient(disabled.URL, ""), "octo", "repo")
	if err != nil {
		t.Fatalf("disabled Stack capability should not fail snapshot: %v", err)
	}
	if len(payloads) != 0 {
		t.Fatalf("disabled Stack payloads = %s, want none", payloads)
	}
}

func TestAGSUploadSkipsEmptySnapshotShards(t *testing.T) {
	dir := t.TempDir()
	if err := os.WriteFile(filepath.Join(dir, "labels-000001.ndjson"), []byte(`{"id":1,"name":"bug"}`+"\n"), 0o644); err != nil {
		t.Fatalf("write labels shard: %v", err)
	}
	if err := os.WriteFile(filepath.Join(dir, "issues-000001.ndjson"), nil, 0o644); err != nil {
		t.Fatalf("write empty issues shard: %v", err)
	}
	manifest := snapshotManifest{
		SchemaVersion: 1,
		Shards: []snapshotShard{
			{ObjectType: "labels", Path: "labels-000001.ndjson", ItemCount: 1, Checksum: "sha256:labels"},
			{ObjectType: "issues", Path: "issues-000001.ndjson", ItemCount: 0, Checksum: "sha256:empty"},
		},
	}
	if err := writeManifest(dir, manifest); err != nil {
		t.Fatalf("write manifest: %v", err)
	}

	var requests []importBatchesRequest
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/api/ext/v1/imports/42/batches/bulk" {
			t.Fatalf("unexpected path %s", r.URL.Path)
		}
		var req importBatchesRequest
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			t.Fatalf("decode request: %v", err)
		}
		requests = append(requests, req)
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"status":"staged"}`))
	}))
	defer server.Close()

	t.Setenv("AGS_TOKEN", "test-token")
	if err := agsUpload(context.Background(), []string{
		"--target", server.URL,
		"--import-id", "42",
		"--snapshot", dir,
	}); err != nil {
		t.Fatalf("agsUpload: %v", err)
	}
	if len(requests) != 1 {
		t.Fatalf("expected one bulk upload, got %d", len(requests))
	}
	if len(requests[0].Batches) != 1 {
		t.Fatalf("expected one non-empty batch in bulk upload, got %+v", requests[0])
	}
	if requests[0].Batches[0].ObjectType != "labels" || len(requests[0].Batches[0].Items) != 1 {
		t.Fatalf("unexpected uploaded batch: %+v", requests[0].Batches[0])
	}
	if requests[0].Batches[0].Checksum != "" {
		t.Fatalf("expected AGS to compute upload checksum, got %q", requests[0].Batches[0].Checksum)
	}
}

func TestAGSUploadCanSendManifestChecksum(t *testing.T) {
	dir := t.TempDir()
	if err := os.WriteFile(filepath.Join(dir, "labels-000001.ndjson"), []byte(`{"id":1,"name":"bug"}`+"\n"), 0o644); err != nil {
		t.Fatalf("write labels shard: %v", err)
	}
	manifest := snapshotManifest{
		SchemaVersion: 1,
		Shards: []snapshotShard{
			{ObjectType: "labels", Path: "labels-000001.ndjson", ItemCount: 1, Checksum: "sha256:labels"},
		},
	}
	if err := writeManifest(dir, manifest); err != nil {
		t.Fatalf("write manifest: %v", err)
	}

	var got importBatchesRequest
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if err := json.NewDecoder(r.Body).Decode(&got); err != nil {
			t.Fatalf("decode request: %v", err)
		}
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"status":"staged"}`))
	}))
	defer server.Close()

	t.Setenv("AGS_TOKEN", "test-token")
	if err := agsUpload(context.Background(), []string{
		"--target", server.URL,
		"--import-id", "42",
		"--snapshot", dir,
		"--send-manifest-checksum",
	}); err != nil {
		t.Fatalf("agsUpload: %v", err)
	}
	if len(got.Batches) != 1 {
		t.Fatalf("expected one batch, got %+v", got)
	}
	if got.Batches[0].Checksum != "sha256:labels" {
		t.Fatalf("expected manifest checksum, got %q", got.Batches[0].Checksum)
	}
}

func TestChunkUploadBatchesHonorsByteLimit(t *testing.T) {
	requests := []importBatchRequest{
		{BatchID: "a", ObjectType: "issues", Items: []batchItem{{Payload: json.RawMessage(`{"body":"aaaaaaaaaa"}`)}}},
		{BatchID: "b", ObjectType: "issues", Items: []batchItem{{Payload: json.RawMessage(`{"body":"bbbbbbbbbb"}`)}}},
		{BatchID: "c", ObjectType: "issues", Items: []batchItem{{Payload: json.RawMessage(`{"body":"cccccccccc"}`)}}},
	}
	chunks := chunkUploadBatches(requests, 10, importBatchRequestPayloadBytes(requests[0])+1)
	if len(chunks) != 3 {
		t.Fatalf("chunk count = %d, want 3", len(chunks))
	}
	for _, chunk := range chunks {
		if len(chunk) != 1 {
			t.Fatalf("expected byte limit to keep one request per chunk, got %+v", chunk)
		}
	}
}

func TestReadBatchItemsSupportsReleaseAssetMetadata(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "release-assets.ndjson")
	payload, err := json.Marshal(map[string]any{"id": 1, "name": "large.tar.gz", "content_size": 17 * 1024 * 1024, "content_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"})
	if err != nil {
		t.Fatalf("marshal payload: %v", err)
	}
	if err := os.WriteFile(path, append(payload, '\n'), 0o600); err != nil {
		t.Fatalf("write payload: %v", err)
	}
	items, err := readBatchItems(path)
	if err != nil {
		t.Fatalf("read release asset metadata: %v", err)
	}
	if len(items) != 1 || len(items[0].Payload) != len(payload) {
		t.Fatalf("items = %d, payload bytes = %d, want one item with %d bytes", len(items), len(items[0].Payload), len(payload))
	}
}

func TestAGSUploadStreamsAssetBeyondNormalJSONLimit(t *testing.T) {
	dir := t.TempDir()
	const assetSize = int64(50<<20 + 1)
	assetPath := filepath.Join(dir, "release-assets", "large")
	if err := os.MkdirAll(filepath.Dir(assetPath), 0o755); err != nil {
		t.Fatalf("make asset directory: %v", err)
	}
	asset, err := os.Create(assetPath)
	if err != nil {
		t.Fatalf("create asset: %v", err)
	}
	if err := asset.Truncate(assetSize); err != nil {
		asset.Close()
		t.Fatalf("size asset: %v", err)
	}
	if err := asset.Close(); err != nil {
		t.Fatalf("close asset: %v", err)
	}
	hash := sha256.New()
	file, err := os.Open(assetPath)
	if err != nil {
		t.Fatalf("open asset: %v", err)
	}
	if _, err := io.Copy(hash, file); err != nil {
		file.Close()
		t.Fatalf("hash asset: %v", err)
	}
	file.Close()
	checksum := "sha256:" + hex.EncodeToString(hash.Sum(nil))
	manifest := snapshotManifest{AssetFiles: []snapshotAssetFile{{SourceID: "large", Path: "release-assets/large", Size: assetSize, Checksum: checksum}}}

	var uploaded int64
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/api/ext/v1/imports/42/release-assets/large" || r.Method != http.MethodPut {
			t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
		}
		if r.ContentLength != assetSize || r.Header.Get("Content-Type") != "application/octet-stream" {
			t.Fatalf("content length/type = %d/%q", r.ContentLength, r.Header.Get("Content-Type"))
		}
		var err error
		uploaded, err = io.Copy(io.Discard, r.Body)
		if err != nil {
			t.Fatalf("read asset body: %v", err)
		}
		_, _ = w.Write([]byte(`{"status":"ok"}`))
	}))
	defer server.Close()

	if err := uploadSnapshotAssetFiles(context.Background(), server.URL+"/api/ext/v1/imports/42", "token", dir, manifest); err != nil {
		t.Fatalf("uploadSnapshotAssetFiles: %v", err)
	}
	if uploaded != assetSize {
		t.Fatalf("uploaded bytes = %d, want %d", uploaded, assetSize)
	}
}

func TestAGSUploadSendsReleaseAssetsAsRawBytesBeforeMetadata(t *testing.T) {
	dir := t.TempDir()
	assetBytes := []byte("release\x00asset")
	assetPath := filepath.Join(dir, "release-assets", "900")
	if err := os.MkdirAll(filepath.Dir(assetPath), 0o755); err != nil {
		t.Fatalf("make asset directory: %v", err)
	}
	if err := os.WriteFile(assetPath, assetBytes, 0o600); err != nil {
		t.Fatalf("write asset: %v", err)
	}
	sum := sha256.Sum256(assetBytes)
	checksum := "sha256:" + hex.EncodeToString(sum[:])
	if err := os.WriteFile(filepath.Join(dir, "release_assets-000001.ndjson"), []byte(`{"id":900,"release_id":1,"name":"asset.bin","content_size":13,"content_sha256":"`+checksum+`"}`+"\n"), 0o600); err != nil {
		t.Fatalf("write metadata shard: %v", err)
	}
	manifest := snapshotManifest{SchemaVersion: 1, AssetFiles: []snapshotAssetFile{{SourceID: "900", Path: "release-assets/900", Size: int64(len(assetBytes)), Checksum: checksum}}, Shards: []snapshotShard{{ObjectType: "release_assets", Path: "release_assets-000001.ndjson", ItemCount: 1}}}
	if err := writeManifest(dir, manifest); err != nil {
		t.Fatalf("write manifest: %v", err)
	}

	var paths []string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		paths = append(paths, r.URL.Path)
		switch r.URL.Path {
		case "/api/ext/v1/imports/42/release-assets/900":
			if r.Method != http.MethodPut || r.Header.Get("Content-Type") != "application/octet-stream" {
				t.Fatalf("release asset request = %s %s", r.Method, r.Header.Get("Content-Type"))
			}
			body, err := io.ReadAll(r.Body)
			if err != nil || string(body) != string(assetBytes) {
				t.Fatalf("release asset bytes = %q, err = %v", body, err)
			}
		case "/api/ext/v1/imports/42/batches/bulk":
			var request importBatchesRequest
			if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
				t.Fatalf("decode metadata: %v", err)
			}
			if len(request.Batches) != 1 || strings.Contains(string(request.Batches[0].Items[0].Payload), "content_base64") {
				t.Fatalf("unexpected metadata payload: %+v", request)
			}
		default:
			t.Fatalf("unexpected path %s", r.URL.Path)
		}
		_, _ = w.Write([]byte(`{"status":"ok"}`))
	}))
	defer server.Close()

	t.Setenv("AGS_TOKEN", "test-token")
	if err := agsUpload(context.Background(), []string{"--target", server.URL, "--import-id", "42", "--snapshot", dir}); err != nil {
		t.Fatalf("agsUpload: %v", err)
	}
	if len(paths) != 2 || !strings.Contains(paths[0], "/release-assets/") {
		t.Fatalf("request order = %v, want asset then metadata", paths)
	}
}

func TestAGSApplyLoopsUntilNoSupportedBatchesRemain(t *testing.T) {
	var requests []importApplyRequest
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/api/ext/v1/imports/42/apply" {
			t.Fatalf("unexpected path %s", r.URL.Path)
		}
		var req importApplyRequest
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			t.Fatalf("decode request: %v", err)
		}
		requests = append(requests, req)
		w.Header().Set("Content-Type", "application/json")
		switch len(requests) {
		case 1:
			_, _ = w.Write([]byte(`{"import_id":42,"applied_batches":1,"applied_items":10,"remaining_supported_batches":1}`))
		case 2:
			_, _ = w.Write([]byte(`{"import_id":42,"applied_batches":1,"applied_items":5,"remaining_supported_batches":0}`))
		default:
			t.Fatalf("unexpected apply request %d", len(requests))
		}
	}))
	defer server.Close()

	t.Setenv("AGS_TOKEN", "test-token")
	if err := agsApply(context.Background(), []string{
		"--target", server.URL,
		"--import-id", "42",
		"--batch-limit", "1",
	}); err != nil {
		t.Fatalf("agsApply: %v", err)
	}
	if len(requests) != 2 {
		t.Fatalf("request count = %d, want 2", len(requests))
	}
	for _, req := range requests {
		if req.BatchLimit != 1 {
			t.Fatalf("batch_limit = %d, want 1", req.BatchLimit)
		}
		if !req.SkipVerify {
			t.Fatalf("skip_verify = false, want true")
		}
	}
}
