package ags

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/glebarez/sqlite"
	agsserver "github.com/pingcap/agent-git-service/server"
	"github.com/stretchr/testify/require"
	"gorm.io/gorm"

	"github.com/tidbcloud/AgentTeam/internal/agsbridge"
	"github.com/tidbcloud/AgentTeam/internal/identity/trusted"
	"github.com/tidbcloud/AgentTeam/internal/service/repotokens"
	ratstore "github.com/tidbcloud/AgentTeam/internal/store/repotokens"
)

type fakeWorkspaceAccessVerifier struct {
	active bool
	err    error
}

func (v fakeWorkspaceAccessVerifier) HasWorkspaceAccess(context.Context, string, string) (bool, error) {
	return v.active, v.err
}

func TestAgentTeamAuthenticatorHonorsMeshXInternalSiteAdminContext(t *testing.T) {
	req := httptest.NewRequest("GET", "/api/v3/user", nil)
	req.Header.Set("Authorization", "Bearer "+jwt(map[string]any{
		"sub":                "meshx-internal",
		"preferred_username": "meshx-internal",
		"name":               "meshx-internal",
	}))
	req = req.WithContext(agsbridge.ContextWithSiteAdmin(req.Context()))

	identity, ok, err := (AgentTeamAuthenticator{Options: trusted.Options{Enabled: true}}).Authenticate(req)

	require.NoError(t, err)
	require.True(t, ok)
	require.Equal(t, "agentteam", identity.Provider)
	require.Equal(t, "meshx-internal", identity.Subject)
	require.Equal(t, "meshx-internal", identity.Login)
	require.True(t, identity.SiteAdmin)
}

func TestAgentTeamAuthenticatorDoesNotGrantSiteAdminWithoutInternalContext(t *testing.T) {
	req := httptest.NewRequest("GET", "/api/v3/user", nil)
	req.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "user", "preferred_username": "user"}))

	identity, ok, err := (AgentTeamAuthenticator{Options: trusted.Options{Enabled: true}}).Authenticate(req)

	require.NoError(t, err)
	require.True(t, ok)
	require.False(t, identity.SiteAdmin)
}

func TestAgentTeamAuthenticatorSkipsNonBearerAuthorization(t *testing.T) {
	req := httptest.NewRequest("GET", "/api/v3/repos/owner/repo", nil)
	req.Header.Set("Authorization", "Basic dXNlcjpwYXNz")

	_, ok, err := (AgentTeamAuthenticator{Options: trusted.Options{Enabled: true}}).Authenticate(req)

	require.NoError(t, err)
	require.False(t, ok)
}

func TestRepositoryTokenAuthenticatorScopesBasicGitCredentialToRepositoryAndCapability(t *testing.T) {
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&ratstore.Token{}, &ratstore.Event{}, &ratstore.AgentSuppression{}))
	now := time.Date(2026, 7, 16, 5, 0, 0, 0, time.UTC)
	tokens := repotokens.NewService(ratstore.New(db), func() time.Time { return now })
	created, err := tokens.CreateHuman(t.Context(), repotokens.CreateHumanRequest{
		WorkspaceID: "wsp_1", ActorPrincipalID: "usr_1", DisplayName: "Git",
		ProviderHost:    "github.com",
		RepositoryScope: repotokens.RepositoryScope{Mode: "repositories", Repositories: []string{"pingcap/agentteam"}},
		Capabilities:    []string{"repo:clone"},
	})
	require.NoError(t, err)
	authenticator := RepositoryTokenAuthenticator{
		AgentTeam: AgentTeamAuthenticator{Options: trusted.Options{Enabled: true}}, Tokens: tokens,
		WorkspaceAccess: fakeWorkspaceAccessVerifier{active: true},
	}

	request := httptest.NewRequest("GET", "/pingcap/agentteam.git/info/refs?service=git-upload-pack", nil)
	request.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("git:"+created.Plaintext)))
	identity, ok, err := authenticator.Authenticate(request)
	require.NoError(t, err)
	require.True(t, ok)
	require.False(t, identity.SiteAdmin)
	require.Equal(t, "agentteam", identity.Provider)
	require.Equal(t, "usr_1", identity.Subject)
	require.Equal(t, "pingcap/agentteam", identity.RepositoryGrant.FullName)
	require.Equal(t, "read", identity.RepositoryGrant.Permission)
	require.True(t, identity.RepositoryGrant.RequireCurrentAccess)

	wrongOwner := request.Clone(request.Context())
	wrongOwner.Header = request.Header.Clone()
	wrongOwner.Header.Set(routeWorkspaceHeader, "wsp_other")
	_, ok, err = authenticator.Authenticate(wrongOwner)
	require.ErrorContains(t, err, "does not match route owner")
	require.True(t, ok)

	wrongRepo := httptest.NewRequest("GET", "/pingcap/private.git/info/refs?service=git-upload-pack", nil)
	wrongRepo.Header.Set("Authorization", request.Header.Get("Authorization"))
	_, ok, err = authenticator.Authenticate(wrongRepo)
	require.Error(t, err)
	require.True(t, ok)

	push := httptest.NewRequest("POST", "/pingcap/agentteam.git/git-receive-pack", nil)
	push.Header.Set("Authorization", request.Header.Get("Authorization"))
	_, ok, err = authenticator.Authenticate(push)
	require.Error(t, err)
	require.True(t, ok)
}

func TestRepositoryTokenAuthenticatorUsesBearerForRepoREST(t *testing.T) {
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&ratstore.Token{}, &ratstore.Event{}, &ratstore.AgentSuppression{}))
	tokens := repotokens.NewService(ratstore.New(db), nil)
	created, err := tokens.CreateSyncLease(t.Context(), repotokens.CreateSyncLeaseRequest{
		WorkspaceID: "wsp_1", ActorPrincipalID: "sync", ProviderHost: "github.com",
		RepositoryScope: repotokens.RepositoryScope{Mode: "repositories", Repositories: []string{"pingcap/agentteam"}},
		Capabilities:    []string{"repo:read"},
	})
	require.NoError(t, err)
	authenticator := RepositoryTokenAuthenticator{Tokens: tokens}
	request := httptest.NewRequest("GET", "/api/v3/repos/pingcap/agentteam/commits", nil)
	request.Header.Set("Authorization", "Bearer "+created.Plaintext)
	_, ok, err := authenticator.Authenticate(request)
	require.NoError(t, err)
	require.True(t, ok)

	write := httptest.NewRequest("PATCH", "/api/v3/repos/pingcap/agentteam", nil)
	write.Header.Set("Authorization", "Bearer "+created.Plaintext)
	_, ok, err = authenticator.Authenticate(write)
	require.Error(t, err)
	require.True(t, ok)
}

func TestRepositoryTokenAuthenticatorFailsClosedWhenHumanOwnerLosesWorkspaceAccess(t *testing.T) {
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&ratstore.Token{}, &ratstore.Event{}, &ratstore.AgentSuppression{}))
	tokens := repotokens.NewService(ratstore.New(db), nil)
	created, err := tokens.CreateHuman(t.Context(), repotokens.CreateHumanRequest{
		WorkspaceID: "wsp_1", ActorPrincipalID: "usr_1", DisplayName: "Git",
		ProviderHost:    "github.com",
		RepositoryScope: repotokens.RepositoryScope{Mode: "repositories", Repositories: []string{"pingcap/agentteam"}},
		Capabilities:    []string{"repo:read"},
	})
	require.NoError(t, err)
	request := httptest.NewRequest(http.MethodGet, "/api/v3/repos/pingcap/agentteam/commits", nil)
	request.Header.Set("Authorization", "Bearer "+created.Plaintext)

	for _, verifier := range []WorkspaceAccessVerifier{
		nil,
		fakeWorkspaceAccessVerifier{active: false},
		fakeWorkspaceAccessVerifier{err: errors.New("lookup unavailable")},
	} {
		authenticator := RepositoryTokenAuthenticator{Tokens: tokens, WorkspaceAccess: verifier}
		_, ok, err := authenticator.Authenticate(request)
		require.True(t, ok)
		require.Error(t, err)
	}
}

func TestRepositoryAuthorizationTargetRejectsRepositoryManagementOperations(t *testing.T) {
	for _, tc := range []struct {
		method string
		path   string
	}{
		{http.MethodDelete, "/api/v3/repos/pingcap/agentteam"},
		{http.MethodPost, "/api/v3/repos/pingcap/agentteam/transfer"},
		{http.MethodPut, "/api/v3/repos/pingcap/agentteam/collaborators/alice"},
		{http.MethodPatch, "/api/v3/repos/pingcap/agentteam"},
		{http.MethodPut, "/api/v3/repos/pingcap/agentteam/actions/secrets/DEPLOY_TOKEN"},
		{http.MethodGet, "/api/v3/repos/pingcap/agentteam/dependabot/secrets"},
		{http.MethodGet, "/api/v3/repos/pingcap/agentteam/dependabot/secrets/public-key"},
		{http.MethodGet, "/api/v3/repos/pingcap/agentteam/dependabot/secrets/DEPLOY_TOKEN"},
		{http.MethodGet, "/api/v3/repos/pingcap/agentteam/codespaces/secrets"},
		{http.MethodGet, "/api/v3/repos/pingcap/agentteam/codespaces/secrets/public-key"},
		{http.MethodGet, "/api/v3/repos/pingcap/agentteam/codespaces/secrets/DEPLOY_TOKEN"},
		{http.MethodPost, "/api/v3/repos/pingcap/agentteam/keys"},
		{http.MethodPut, "/api/v3/repos/pingcap/agentteam/branches/main/protection"},
	} {
		t.Run(tc.method+" "+tc.path, func(t *testing.T) {
			request := httptest.NewRequest(tc.method, tc.path, nil)
			_, _, ok := agsserver.RepositoryAuthorizationTarget(request)
			require.False(t, ok)
		})
	}
}

func TestRepositoryAuthorizationTargetAllowsRepositoryDataOperations(t *testing.T) {
	for _, tc := range []struct {
		method, path, capability string
	}{
		{http.MethodGet, "/api/v3/repos/pingcap/agentteam/commits", "repo:read"},
		{http.MethodGet, "/api/ext/v1/repos/pingcap/agentteam/summary", "repo:read"},
		{http.MethodGet, "/api/v3/repos/pingcap/agentteam/branches/main", "repo:read"},
		{http.MethodPut, "/api/v3/repos/pingcap/agentteam/contents/README.md", "repo:write"},
		{http.MethodPost, "/api/v3/repos/pingcap/agentteam/git/refs", "repo:write"},
		{http.MethodPost, "/api/v3/repos/pingcap/agentteam/issues", "repo:write"},
		{http.MethodPatch, "/api/v3/repos/pingcap/agentteam/pulls/42", "repo:write"},
		{http.MethodPut, "/api/ext/v1/repos/pingcap/agentteam/wiki/pages/Home", "repo:write"},
		{http.MethodGet, "/api/v3/repos/pingcap/agentteam/actions/workflows/build.yml", "repo:read"},
		{http.MethodGet, "/api/v3/repos/pingcap/agentteam/actions/runs/42", "repo:read"},
	} {
		t.Run(tc.method+" "+tc.path, func(t *testing.T) {
			request := httptest.NewRequest(tc.method, tc.path, nil)
			repository, capability, ok := agsserver.RepositoryAuthorizationTarget(request)
			require.True(t, ok)
			require.Equal(t, "pingcap/agentteam", repository)
			require.Equal(t, tc.capability, capability)
		})
	}
}

func jwt(claims map[string]any) string {
	header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none"}`))
	payload, _ := json.Marshal(claims)
	return header + "." + base64.RawURLEncoding.EncodeToString(payload) + "."
}
