package server

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

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

	"github.com/tidbcloud/AgentTeam/internal/config"
	"github.com/tidbcloud/AgentTeam/internal/service/repotokens"
	ratstore "github.com/tidbcloud/AgentTeam/internal/store/repotokens"
)

func doEmbeddedAGSJSONRequest(t *testing.T, handler http.Handler, method, path, auth string, body any) *httptest.ResponseRecorder {
	t.Helper()
	var payload []byte
	var err error
	if body != nil {
		payload, err = json.Marshal(body)
		require.NoError(t, err)
	}
	req := httptest.NewRequest(method, path, bytes.NewReader(payload))
	if auth != "" {
		req.Header.Set("Authorization", auth)
	}
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	recorder := httptest.NewRecorder()
	handler.ServeHTTP(recorder, req)
	return recorder
}

func repositoryTokenSecurityServer(t *testing.T) (*Server, string) {
	t.Helper()
	agentteamCoreDSN := "file:" + t.TempDir() + "/agentteam-core.db?cache=shared"
	agentteamCoreDB, err := gorm.Open(sqlite.Open(agentteamCoreDSN), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, agentteamCoreDB.Exec(`
CREATE TABLE members (
  id text PRIMARY KEY,
  workspace_id text NOT NULL,
  type text NOT NULL,
  user_id text,
  status text NOT NULL
)`).Error)

	srv, err := New(config.ResourceRuntimeConfig{
		HTTPAddress: ":0",
		Auth:        config.ResourceRuntimeAuthConfig{Enabled: true, Header: "Authorization"},
		Database:    config.ResourceDatabaseConfig{DSN: meshxDatabaseDSN(t)},
		AgentTeamCore:    config.AgentTeamCoreConfig{DatabaseDSN: agentteamCoreDSN},
		AGS: config.AGSConfig{
			DatabaseDSN: agsFileDatabaseDSN(t),
			GitRepoDir:  t.TempDir(),
			BaseURL:     "http://127.0.0.1:3002",
		},
	})
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, srv.Shutdown(context.Background())) })
	return srv, agentteamCoreDSN
}

func TestRepositoryWriteTokenCannotReachRepositoryManagementRoutes(t *testing.T) {
	requireCGO(t)
	srv, _ := repositoryTokenSecurityServer(t)

	const repoFull = "rat-owner/security-boundary"
	ownerAuth := "Bearer " + jwt(map[string]any{
		"sub": "rat-owner", "preferred_username": "rat-owner",
	})
	create := doEmbeddedAGSJSONRequest(t, srv.handler, http.MethodPost, "/api/v3/user/repos", ownerAuth, map[string]any{
		"name": "security-boundary", "auto_init": true,
	})
	require.Equal(t, http.StatusCreated, create.Code, create.Body.String())

	tokens := repotokens.NewService(ratstore.New(srv.meshxDB), nil)
	lease, err := tokens.CreateSyncLease(t.Context(), repotokens.CreateSyncLeaseRequest{
		WorkspaceID: "wsp_1", ActorPrincipalID: "sync", ProviderHost: "github.com",
		RepositoryScope: repotokens.RepositoryScope{Mode: "repositories", Repositories: []string{repoFull}},
		Capabilities:    []string{"repo:write"},
	})
	require.NoError(t, err)
	ratAuth := "Bearer " + lease.Plaintext

	content := doEmbeddedAGSJSONRequest(t, srv.handler, http.MethodPut, "/api/v3/repos/"+repoFull+"/contents/allowed.txt", ratAuth, map[string]any{
		"message": "write through repository data plane",
		"content": "YWxsb3dlZA==",
	})
	require.Equal(t, http.StatusCreated, content.Code, content.Body.String())

	for _, tc := range []struct {
		method string
		path   string
		body   map[string]any
	}{
		{http.MethodDelete, "/api/v3/repos/" + repoFull, nil},
		{http.MethodPost, "/api/v3/repos/" + repoFull + "/transfer", map[string]any{"new_owner": "other"}},
		{http.MethodPut, "/api/v3/repos/" + repoFull + "/collaborators/alice", map[string]any{"permission": "write"}},
		{http.MethodPatch, "/api/v3/repos/" + repoFull, map[string]any{"private": false}},
		{http.MethodPut, "/api/v3/repos/" + repoFull + "/actions/secrets/DEPLOY_TOKEN", map[string]any{"encrypted_value": "secret"}},
		{http.MethodGet, "/api/v3/repos/" + repoFull + "/dependabot/secrets", nil},
		{http.MethodGet, "/api/v3/repos/" + repoFull + "/dependabot/secrets/public-key", nil},
		{http.MethodGet, "/api/v3/repos/" + repoFull + "/dependabot/secrets/DEPLOY_TOKEN", nil},
		{http.MethodGet, "/api/v3/repos/" + repoFull + "/codespaces/secrets", nil},
		{http.MethodGet, "/api/v3/repos/" + repoFull + "/codespaces/secrets/public-key", nil},
		{http.MethodGet, "/api/v3/repos/" + repoFull + "/codespaces/secrets/DEPLOY_TOKEN", nil},
	} {
		rec := doEmbeddedAGSJSONRequest(t, srv.handler, tc.method, tc.path, ratAuth, tc.body)
		require.Contains(t, []int{http.StatusUnauthorized, http.StatusForbidden}, rec.Code, "%s %s: %s", tc.method, tc.path, rec.Body.String())
	}

	get := doEmbeddedAGSJSONRequest(t, srv.handler, http.MethodGet, "/api/v3/repos/"+repoFull, ownerAuth, nil)
	require.Equal(t, http.StatusOK, get.Code, get.Body.String())
}

func TestHumanRepositoryTokenFailsImmediatelyAfterCurrentAccessIsRevoked(t *testing.T) {
	requireCGO(t)
	srv, agentteamCoreDSN := repositoryTokenSecurityServer(t)

	const (
		workspaceID = "wsp_1"
		repoFull    = "rat-owner/current-access"
	)
	ownerAuth := "Bearer " + jwt(map[string]any{"sub": "rat-owner", "preferred_username": "rat-owner"})
	memberAuth := "Bearer " + jwt(map[string]any{"sub": "rat-member", "preferred_username": "rat-member"})
	create := doEmbeddedAGSJSONRequest(t, srv.handler, http.MethodPost, "/api/v3/user/repos", ownerAuth, map[string]any{
		"name": "current-access", "auto_init": true, "private": true,
	})
	require.Equal(t, http.StatusCreated, create.Code, create.Body.String())
	bindMember := doEmbeddedAGSJSONRequest(t, srv.handler, http.MethodGet, "/api/v3/user", memberAuth, nil)
	require.Equal(t, http.StatusOK, bindMember.Code, bindMember.Body.String())

	grantor, ok := srv.ags.(agsRepositoryCollaboratorGrantor)
	require.True(t, ok)
	revoker, ok := srv.ags.(agsRepositoryCollaboratorRevoker)
	require.True(t, ok)
	member := agsauth.Identity{Provider: "agentteam", Subject: "rat-member", Login: "rat-member"}
	_, err := grantor.GrantRepositoryCollaborator(t.Context(), agsserver.RepositoryCollaboratorGrantRequest{
		Owner: "rat-owner", Repo: "current-access", Permission: "read", User: member,
	})
	require.NoError(t, err)

	agentteamCoreDB, err := gorm.Open(sqlite.Open(agentteamCoreDSN), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, agentteamCoreDB.Exec(`
INSERT INTO members (id, workspace_id, type, user_id, status)
VALUES ('mem_rat', ?, 'human', 'rat-member', 'active')
`, workspaceID).Error)

	tokens := repotokens.NewService(ratstore.New(srv.meshxDB), nil)
	created, err := tokens.CreateHuman(t.Context(), repotokens.CreateHumanRequest{
		WorkspaceID: workspaceID, ActorPrincipalID: "rat-member", DisplayName: "Current access",
		ProviderHost:    "github.com",
		RepositoryScope: repotokens.RepositoryScope{Mode: "repositories", Repositories: []string{repoFull}},
		Capabilities:    []string{"repo:read"},
	})
	require.NoError(t, err)
	ratAuth := "Bearer " + created.Plaintext
	read := doEmbeddedAGSJSONRequest(t, srv.handler, http.MethodGet, "/api/v3/repos/"+repoFull, ratAuth, nil)
	require.Equal(t, http.StatusOK, read.Code, read.Body.String())

	require.NoError(t, revoker.RevokeRepositoryCollaborator(t.Context(), agsserver.RepositoryCollaboratorRevokeRequest{
		Owner: "rat-owner", Repo: "current-access", User: member,
	}))
	read = doEmbeddedAGSJSONRequest(t, srv.handler, http.MethodGet, "/api/v3/repos/"+repoFull, ratAuth, nil)
	require.Equal(t, http.StatusUnauthorized, read.Code, read.Body.String())

	public := doEmbeddedAGSJSONRequest(t, srv.handler, http.MethodPatch, "/api/v3/repos/"+repoFull, ownerAuth, map[string]any{"private": false})
	require.Equal(t, http.StatusOK, public.Code, public.Body.String())
	read = doEmbeddedAGSJSONRequest(t, srv.handler, http.MethodGet, "/api/v3/repos/"+repoFull, ratAuth, nil)
	require.Equal(t, http.StatusUnauthorized, read.Code, read.Body.String())

	_, err = grantor.GrantRepositoryCollaborator(t.Context(), agsserver.RepositoryCollaboratorGrantRequest{
		Owner: "rat-owner", Repo: "current-access", Permission: "read", User: member,
	})
	require.NoError(t, err)
	require.NoError(t, agentteamCoreDB.Exec(`UPDATE members SET status = 'inactive' WHERE id = 'mem_rat'`).Error)
	read = doEmbeddedAGSJSONRequest(t, srv.handler, http.MethodGet, "/api/v3/repos/"+repoFull, ratAuth, nil)
	require.Equal(t, http.StatusUnauthorized, read.Code, read.Body.String())
}
