package kb

import (
	"encoding/base64"
	"encoding/json"
	"net/http"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/contentpayload"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
)

func TestAGSExactKBSourceExporterReadsHistoricalCommitThroughExistingBoundedAPIs(t *testing.T) {
	readme := []byte("historical")
	config := []byte("enabled=true\n")
	entries := []testKBSourceTreeEntry{
		{Path: "config.txt", Mode: "100644", Data: config},
		{Path: "README.md", Mode: "100755", Data: readme},
	}
	var repositoryReads, treeReads, blobReads atomic.Int64
	exporter := AGSExactKBSourceExporter{Handler: exactKBSourceReadHandler("42", "main", strings.Repeat("a", 40), entries, &repositoryReads, &treeReads, &blobReads)}
	request := KBSourceContentExportRequest{
		Repository: repos.RepositoryIdentity{ID: "42", FullName: "org/repo"},
		// The requested commit deliberately differs from the live branch head;
		// historical migration reads the immutable tree directly.
		CanonicalBranch: "main", CommitSHA: strings.Repeat("b", 40), Limits: testKBSourceMigrationLimits(),
	}
	exported, err := exporter.ExportExactKBSource(t.Context(), request)
	require.NoError(t, err)
	require.Equal(t, int64(1), repositoryReads.Load())
	require.Equal(t, int64(1), treeReads.Load())
	require.Equal(t, int64(2), blobReads.Load())
	require.Equal(t, []string{"README.md", "config.txt"}, []string{exported.Files[0].Path, exported.Files[1].Path})
	require.True(t, exported.Files[0].Executable)
	require.Equal(t, readme, exported.Files[0].Data)
	require.NotEmpty(t, exported.RootDigest)
	require.Equal(t, "2026-08-20T01:02:03Z", exported.SourceCommittedAt.Format(time.RFC3339))
}

func TestAGSExactKBSourceExporterRejectsIdentityAndUnboundedTreeBeforeBlobReads(t *testing.T) {
	data := []byte("data")
	regular := []testKBSourceTreeEntry{{Path: "README.md", Mode: "100644", Data: data}}
	baseRequest := KBSourceContentExportRequest{
		Repository:      repos.RepositoryIdentity{ID: "42", FullName: "org/repo"},
		CanonicalBranch: "main", CommitSHA: strings.Repeat("b", 40), Limits: testKBSourceMigrationLimits(),
	}
	tests := []struct {
		name    string
		handler func(*atomic.Int64) http.Handler
	}{
		{name: "live repository id changed", handler: func(blobs *atomic.Int64) http.Handler {
			return exactKBSourceReadHandler("43", "main", strings.Repeat("a", 40), regular, nil, nil, blobs)
		}},
		{name: "canonical branch changed", handler: func(blobs *atomic.Int64) http.Handler {
			return exactKBSourceReadHandler("42", "master", strings.Repeat("a", 40), regular, nil, nil, blobs)
		}},
		{name: "gitlink", handler: func(blobs *atomic.Int64) http.Handler {
			return malformedKBSourceTreeHandler([]map[string]any{{"path": "vendor/module", "mode": "160000", "type": "commit", "sha": strings.Repeat("c", 40), "size": 0}}, blobs)
		}},
		{name: "symlink", handler: func(blobs *atomic.Int64) http.Handler {
			return malformedKBSourceTreeHandler([]map[string]any{{"path": "link", "mode": "120000", "type": "blob", "sha": strings.Repeat("d", 40), "size": 4}}, blobs)
		}},
		{name: "oversized tree", handler: func(blobs *atomic.Int64) http.Handler {
			return malformedKBSourceTreeHandler([]map[string]any{{"path": "huge", "mode": "100644", "type": "blob", "sha": strings.Repeat("e", 40), "size": testKBSourceMigrationLimits().MaxTotalBytes + 1}}, blobs)
		}},
		{name: "oversized metadata", handler: func(_ *atomic.Int64) http.Handler {
			return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(strings.Repeat("x", 65<<10))) })
		}},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			var blobReads atomic.Int64
			exporter := AGSExactKBSourceExporter{Handler: test.handler(&blobReads)}
			_, err := exporter.ExportExactKBSource(t.Context(), baseRequest)
			require.Error(t, err)
			require.Zero(t, blobReads.Load())
		})
	}
}

func TestAGSExactKBSourceExporterRejectsBlobChangedAfterPreflight(t *testing.T) {
	data := []byte("expected")
	entry := testKBSourceTreeEntry{Path: "README.md", Mode: "100644", Data: data}
	handler := exactKBSourceReadHandler("42", "main", strings.Repeat("a", 40), []testKBSourceTreeEntry{entry}, nil, nil, nil)
	corrupt := http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
		if strings.Contains(request.URL.Path, "/git/blobs/") {
			_ = json.NewEncoder(w).Encode(map[string]any{
				"sha": gitBlobSHA(data), "size": len(data), "encoding": "base64",
				"content": base64.StdEncoding.EncodeToString([]byte("changed!")),
			})
			return
		}
		handler.ServeHTTP(w, request)
	})
	exporter := AGSExactKBSourceExporter{Handler: corrupt}
	_, err := exporter.ExportExactKBSource(t.Context(), KBSourceContentExportRequest{
		Repository:      repos.RepositoryIdentity{ID: "42", FullName: "org/repo"},
		CanonicalBranch: "main", CommitSHA: strings.Repeat("b", 40), Limits: testKBSourceMigrationLimits(),
	})
	require.ErrorContains(t, err, "digest changed")
}

type testKBSourceTreeEntry struct {
	Path string
	Mode string
	Data []byte
}

func exactKBSourceReadHandler(repositoryID, branch, liveCommit string, entries []testKBSourceTreeEntry, repositoryReads, treeReads, blobReads *atomic.Int64) http.Handler {
	bySHA := make(map[string]testKBSourceTreeEntry, len(entries))
	treeEntries := make([]map[string]any, 0, len(entries))
	for _, entry := range entries {
		sha := gitBlobSHA(entry.Data)
		bySHA[sha] = entry
		treeEntries = append(treeEntries, map[string]any{"path": entry.Path, "mode": entry.Mode, "type": "blob", "sha": sha, "size": len(entry.Data)})
	}
	return http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		switch {
		case strings.Contains(request.URL.Path, "/git/trees/"):
			if treeReads != nil {
				treeReads.Add(1)
			}
			_ = json.NewEncoder(w).Encode(map[string]any{"sha": liveCommit, "truncated": false, "tree": treeEntries})
		case strings.Contains(request.URL.Path, "/git/commits/"):
			sha := request.URL.Path[strings.LastIndex(request.URL.Path, "/")+1:]
			_ = json.NewEncoder(w).Encode(map[string]any{"sha": sha, "author": map[string]any{"date": "2026-08-20T01:02:03Z"}, "committer": map[string]any{"date": "2026-08-20T01:02:03Z"}})
		case strings.Contains(request.URL.Path, "/git/blobs/"):
			if blobReads != nil {
				blobReads.Add(1)
			}
			sha := request.URL.Path[strings.LastIndex(request.URL.Path, "/")+1:]
			entry, ok := bySHA[sha]
			if !ok {
				http.NotFound(w, request)
				return
			}
			_ = json.NewEncoder(w).Encode(map[string]any{"sha": sha, "size": len(entry.Data), "encoding": "base64", "content": base64.StdEncoding.EncodeToString(entry.Data)})
		default:
			if repositoryReads != nil {
				repositoryReads.Add(1)
			}
			_ = json.NewEncoder(w).Encode(map[string]any{"id": repositoryID, "default_branch": branch})
		}
	})
}

func malformedKBSourceTreeHandler(entries []map[string]any, blobReads *atomic.Int64) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
		switch {
		case strings.Contains(request.URL.Path, "/git/trees/"):
			_ = json.NewEncoder(w).Encode(map[string]any{"sha": strings.Repeat("a", 40), "truncated": false, "tree": entries})
		case strings.Contains(request.URL.Path, "/git/commits/"):
			sha := request.URL.Path[strings.LastIndex(request.URL.Path, "/")+1:]
			_ = json.NewEncoder(w).Encode(map[string]any{"sha": sha, "author": map[string]any{"date": "2026-08-20T01:02:03Z"}, "committer": map[string]any{"date": "2026-08-20T01:02:03Z"}})
		case strings.Contains(request.URL.Path, "/git/blobs/"):
			blobReads.Add(1)
			http.NotFound(w, request)
		default:
			_ = json.NewEncoder(w).Encode(map[string]any{"id": "42", "default_branch": "main"})
		}
	})
}

func testKBSourceMigrationLimits() contentpayload.Limits {
	return contentpayload.Limits{MaxFiles: 16, MaxFileBytes: 2 << 20, MaxTotalBytes: 4 << 20, MaxManifestBytes: 256 << 10}
}
