package server

import (
	"net/http"
	"net/http/httptest"
	"testing"

	routecontract "github.com/pingcap/agent-git-service/repositoryroutecontract"
	"github.com/stretchr/testify/require"
)

func TestMatchRegisteredRouteUsesRealAGSRegistration(t *testing.T) {
	t.Parallel()

	for _, test := range []struct {
		method, path, pattern string
	}{
		{http.MethodGet, "/api/v3/user/repos", "/api/v3/user/repos"},
		{http.MethodGet, "/api/v3/repos/acme/docs/contents/README.md", "/api/v3/repos/{owner}/{repo}/contents/*"},
		{http.MethodGet, "/acme/docs.git/info/refs", "/{owner}/{repo}/info/refs"},
		{http.MethodPost, "/acme/docs.git/git-upload-pack", "/{owner}/{repo}/git-upload-pack"},
	} {
		match, ok := MatchRegisteredRoute(httptest.NewRequest(test.method, test.path, nil))
		require.True(t, ok, test.path)
		require.Equal(t, test.pattern, match.Pattern, test.path)
	}

	for _, test := range []struct{ method, path string }{
		{http.MethodPost, "/api/v3/user/repos/unknown"},
		{http.MethodGet, "/api/v3/repos/acme/docs/not-registered"},
		{http.MethodGet, "/acme/docs.git/unknown"},
		{http.MethodOptions, "/api/v3/repos/acme/docs"},
	} {
		_, ok := MatchRegisteredRoute(httptest.NewRequest(test.method, test.path, nil))
		require.False(t, ok, test.path)
	}

	require.NotEmpty(t, RegisteredRoutes())
}

func TestRegisteredRouteMetadataAndSplitOverlayStaySeparated(t *testing.T) {
	t.Parallel()

	for _, test := range []struct {
		method, target, repository, capability string
		operation                              routecontract.Operation
		authentication                         routecontract.AuthenticationScope
		authority                              routecontract.AuthorityMode
	}{
		{http.MethodGet, "/api/v3/user/repos", "", "", routecontract.OperationViewerRepos, routecontract.AuthenticationAuthenticated, routecontract.AuthoritySessionWorkspace},
		{http.MethodGet, "/api/v3/search/code", "", "", routecontract.OperationCodeSearch, routecontract.AuthenticationAuthenticated, routecontract.AuthoritySessionWorkspace},
		{http.MethodGet, "/api/v3/repos/acme/docs/contents/README.md", "acme/docs", "repo:read", routecontract.Operation("GET /api/v3/repos/{owner}/{repo}/contents/*"), routecontract.AuthenticationOptional, routecontract.AuthorityRepositoryCredentialOrSession},
		{http.MethodPut, "/api/v3/repos/acme/docs/contents/README.md", "acme/docs", "repo:write", routecontract.Operation("PUT /api/v3/repos/{owner}/{repo}/contents/*"), routecontract.AuthenticationOptional, routecontract.AuthorityRepositoryCredentialOrSession},
		{http.MethodGet, "/api/ext/v1/repos/acme/docs/summary", "acme/docs", "repo:read", routecontract.Operation("GET /api/ext/v1/repos/{owner}/{repo}/summary"), routecontract.AuthenticationOptional, routecontract.AuthorityRepositoryCredentialOrSession},
		{http.MethodGet, "/acme/docs.git/info/refs?service=git-upload-pack", "acme/docs", "repo:clone", routecontract.OperationGitInfoRefs, routecontract.AuthenticationOptional, routecontract.AuthorityGitCredentialOnly},
		{http.MethodGet, "/acme/docs.git/info/refs?service=git-receive-pack", "acme/docs", "repo:write", routecontract.OperationGitInfoRefs, routecontract.AuthenticationOptional, routecontract.AuthorityGitCredentialOnly},
		{http.MethodPost, "/acme/docs.git/git-upload-pack", "acme/docs", "repo:clone", routecontract.OperationGitUploadPack, routecontract.AuthenticationOptional, routecontract.AuthorityGitCredentialOnly},
		{http.MethodGet, "/avatars/alice", "", "", routecontract.OperationWorkspaceAvatar, routecontract.AuthenticationPublic, routecontract.AuthorityPublicWorkspaceDiscovery},
	} {
		request := httptest.NewRequest(test.method, test.target, nil)
		match, ok := MatchRegisteredRoute(request)
		require.True(t, ok, test.target)
		require.Equal(t, test.operation, match.Operation, test.target)
		require.Equal(t, test.authentication, match.Authentication, test.target)
		require.Equal(t, test.repository, match.Repository, test.target)
		require.Equal(t, test.capability, match.Capability, test.target)
		authority, activated := routecontract.ActivatedAuthority(match.Operation)
		require.True(t, activated, test.target)
		require.Equal(t, test.authority, authority, test.target)
	}

	for _, test := range []struct {
		method, target string
		activated      bool
	}{
		{http.MethodGet, "/api/v3/user", false},
		{http.MethodGet, "/api/v3/repos/acme/docs/collaborators", false},
		{http.MethodPost, "/api/v3/repos/acme/docs/pages", false},
		{http.MethodGet, "/acme/docs.git/info/refs?service=unknown", true},
	} {
		match, ok := MatchRegisteredRoute(httptest.NewRequest(test.method, test.target, nil))
		require.True(t, ok, "route remains an all-profile AGS registration: %s", test.target)
		require.NotEqual(t, routecontract.OperationUnsupported, match.Operation, test.target)
		require.NotEqual(t, routecontract.AuthenticationUnsupported, match.Authentication, test.target)
		authority, activated := routecontract.ActivatedAuthority(match.Operation)
		require.Equal(t, test.activated, activated, test.target)
		if activated {
			scope := routecontract.ScopeFromRegistration(match.Operation, match.Authentication, authority, match.Repository, match.Capability)
			require.Equal(t, routecontract.ScopeUnsupported, scope.EdgePolicy, test.target)
		}
	}
}

func TestSplitActivationKeysHaveBidirectionalRegistrationCoverage(t *testing.T) {
	t.Parallel()

	registrations := RegisteredRoutes()
	seenRegistration := make(map[string]struct{}, len(registrations))
	coverage := make(map[routecontract.ActivationKey]int)
	seenOperation := make(map[routecontract.Operation]struct{}, len(registrations))
	for _, registration := range registrations {
		key := registration.Method + "\x00" + registration.Pattern
		_, duplicate := seenRegistration[key]
		require.False(t, duplicate, "duplicate AGS registration metadata for %s %s", registration.Method, registration.Pattern)
		seenRegistration[key] = struct{}{}
		_, duplicateOperation := seenOperation[registration.Operation]
		require.False(t, duplicateOperation, "operation must identify exactly one AGS registration: %s", registration.Operation)
		seenOperation[registration.Operation] = struct{}{}
		require.Equal(t, routecontract.OperationForRegistration(registration.Method, registration.Pattern), registration.Operation)
		require.NotEqual(t, routecontract.AuthenticationUnsupported, registration.Authentication, registration.Operation)

		authority, activated := routecontract.ActivatedAuthority(registration.Operation)
		activation := routecontract.ActivationKey{Operation: registration.Operation, Authority: authority}
		if activated {
			coverage[activation]++
			require.NotEqual(t, routecontract.OperationUnsupported, registration.Operation)
			require.NotEqual(t, routecontract.AuthorityUnsupported, authority)
			switch authority {
			case routecontract.AuthoritySessionWorkspace:
				require.Equal(t, routecontract.AuthenticationAuthenticated, registration.Authentication, registration.Operation)
			case routecontract.AuthorityRepositoryCredentialOrSession:
				require.Contains(t, []routecontract.AuthenticationScope{
					routecontract.AuthenticationOptional,
					routecontract.AuthenticationAuthenticated,
				}, registration.Authentication, registration.Operation)
				require.NotEmpty(t, registration.Capability, registration.Operation)
			case routecontract.AuthorityGitCredentialOnly:
				require.Equal(t, routecontract.AuthenticationOptional, registration.Authentication, registration.Operation)
				require.NotEmpty(t, registration.Capability, registration.Operation)
			case routecontract.AuthorityPublicWorkspaceDiscovery:
				require.Equal(t, routecontract.AuthenticationPublic, registration.Authentication, registration.Operation)
			default:
				t.Fatalf("activated registration has unsupported authority: %s", registration.Operation)
			}
		}
	}
	activations := routecontract.ActivatedOperations()
	require.Len(t, coverage, len(activations))
	for _, activation := range activations {
		require.Equal(t, 1, coverage[activation], "activation must map to exactly one real AGS method-pattern registration: %+v", activation)
		require.True(t, routecontract.IsActivatedOperation(activation.Operation, activation.Authority))
		require.False(t, routecontract.IsActivatedOperation(activation.Operation, routecontract.AuthorityUnsupported))
	}
}
