package workspace_test

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	. "github.com/tidbcloud/AgentTeam/internal/api/testkit"
	"github.com/tidbcloud/AgentTeam/internal/core/secrets"
)

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

	runtime := StartRuntime(t)

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()

	var auth dto.AuthTokensResponse
	response := DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/register", map[string]any{
		"email":    "admin@example.com",
		"password": TestUserPassword,
		"name":     "admin",
	}, nil, &auth)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var workspaces []dto.WorkspaceResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, map[string]string{
		"Authorization": fmt.Sprintf("Bearer %s", auth.AccessToken),
	}, &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)

	workspaceID := workspaces[0].ID

	var created dto.CreateMachineResponse
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/machines", map[string]any{
		"name": "dev-machine",
	}, map[string]string{
		"Authorization": fmt.Sprintf("Bearer %s", auth.AccessToken),
	}, &created)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, created.APIKey)
	require.NotEmpty(t, created.Machine.ID)
	require.NotNil(t, created.Machine.Runtimes)
	require.NotNil(t, created.Machine.RuntimeDefaults)

	expectedPrefix := created.APIKey
	if len(expectedPrefix) > 20 {
		expectedPrefix = expectedPrefix[:20]
	}
	require.Equal(t, expectedPrefix, created.Machine.APIKeyPrefix)

	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/machines", nil, map[string]string{
		"Authorization": fmt.Sprintf("Bearer %s", auth.AccessToken),
	}, nil)
	require.Equal(t, http.StatusOK, response.StatusCode)

	body, err := io.ReadAll(response.Body)
	require.NoError(t, err)

	var listResponse dto.MachinesListResponse
	require.NoError(t, json.Unmarshal(body, &listResponse))
	require.Len(t, listResponse.Machines, 1)
	require.Equal(t, created.Machine.ID, listResponse.Machines[0].ID)
	require.Equal(t, "offline", listResponse.Machines[0].Status)
	require.Equal(t, expectedPrefix, listResponse.Machines[0].APIKeyPrefix)
	require.NotNil(t, listResponse.Machines[0].Runtimes)
	require.NotNil(t, listResponse.Machines[0].RuntimeDefaults)

	var raw map[string]any
	require.NoError(t, json.Unmarshal(body, &raw))
	_, ok := raw["apiKey"]
	require.False(t, ok, "machine list must not leak full API keys")
	rawMachines, ok := raw["machines"].([]any)
	require.True(t, ok)
	require.Len(t, rawMachines, 1)
	rawMachine, ok := rawMachines[0].(map[string]any)
	require.True(t, ok)
	require.IsType(t, []any{}, rawMachine["runtimes"])
	require.IsType(t, map[string]any{}, rawMachine["runtimeDefaults"])
}

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

	runtime := StartRuntime(t)

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()

	var auth dto.AuthTokensResponse
	response := DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/register", map[string]any{
		"email":    "connect-owner@example.com",
		"password": TestUserPassword,
		"name":     "connect-owner",
	}, nil, &auth)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var workspaces []dto.WorkspaceResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, AuthHeaders(auth.AccessToken), &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID

	var created dto.CreateMachineResponse
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/machines", map[string]any{
		"name": "browser-machine",
	}, AuthHeaders(auth.AccessToken), &created)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var tokenResponse dto.MachineConnectTokenResponse
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/machines/"+created.Machine.ID+"/connect-tokens", map[string]any{
		"serverUrl": server.URL,
	}, AuthHeaders(auth.AccessToken), &tokenResponse)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, tokenResponse.Token)

	var exchangeResponse dto.MachineConnectExchangeResponse
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/machine-connect-exchanges", map[string]any{
		"token": tokenResponse.Token,
	}, nil, &exchangeResponse)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, created.Machine.ID, exchangeResponse.MachineID)
	require.Equal(t, created.APIKey, exchangeResponse.APIKey)
	require.Equal(t, server.URL, exchangeResponse.ServerURL)
	require.Equal(t, workspaceID, exchangeResponse.WorkspaceID)

}

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

	runtime := StartRuntime(t)

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()

	var owner dto.AuthTokensResponse
	response := DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/register", map[string]any{
		"email":    "owner-connect@example.com",
		"password": TestUserPassword,
		"name":     "owner",
	}, nil, &owner)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var workspaces []dto.WorkspaceResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, AuthHeaders(owner.AccessToken), &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	workspaceID := workspaces[0].ID

	var created dto.CreateMachineResponse
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/machines", map[string]any{
		"name": "owner-machine",
	}, AuthHeaders(owner.AccessToken), &created)
	require.Equal(t, http.StatusOK, response.StatusCode)

	_, err := runtime.Store.CreateWorkspaceInvite(runtime.Ctx, owner.User.ID, workspaceID, "member-connect@example.com", secrets.HashToken("member-connect-token"), time.Now().UTC().Add(time.Hour))
	require.NoError(t, err)

	var member dto.AuthTokensResponse
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/register", map[string]any{
		"email":    "member-connect@example.com",
		"password": TestUserPassword,
		"name":     "member",
	}, nil, &member)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var apiErr struct {
		Error string `json:"error"`
	}
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/machines/"+created.Machine.ID+"/connect-tokens", map[string]any{
		"serverUrl": server.URL,
	}, AuthHeaders(member.AccessToken), &apiErr)
	require.Equal(t, http.StatusForbidden, response.StatusCode)
}

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

	runtime := StartRuntime(t)

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()

	var owner dto.AuthTokensResponse
	response := DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/register", map[string]any{
		"email":    "owner-machine-scope@example.com",
		"password": TestUserPassword,
		"name":     "owner-scope",
	}, nil, &owner)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var workspaces []dto.WorkspaceResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, AuthHeaders(owner.AccessToken), &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID

	inviteToken := "tok_machine_scope_admin"
	_, err := runtime.Store.CreateWorkspaceInvite(
		runtime.Ctx,
		owner.User.ID,
		workspaceID,
		"workspace-admin-machine-scope@example.com",
		secrets.HashToken(inviteToken),
		time.Now().UTC().Add(10*time.Minute),
	)
	require.NoError(t, err)

	var admin dto.AuthTokensResponse
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/register", map[string]any{
		"email":    "workspace-admin-machine-scope@example.com",
		"password": TestUserPassword,
		"name":     "workspace-admin-scope",
	}, nil, &admin)
	require.Equal(t, http.StatusOK, response.StatusCode)

	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/accept-invite", map[string]any{
		"token": inviteToken,
	}, AuthHeaders(admin.AccessToken), nil)
	require.Equal(t, http.StatusOK, response.StatusCode)

	adminMemberID := requireWorkspaceMemberIDForUser(t, client, server.URL, owner.AccessToken, workspaceID, admin.User.ID)
	response = DoJSON(t, client, http.MethodPatch, server.URL+"/api/v1/workspaces/"+workspaceID+"/members/"+adminMemberID, map[string]any{
		"role": "admin",
	}, AuthHeaders(owner.AccessToken), nil)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var ownerMachine dto.CreateMachineResponse
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/machines", map[string]any{
		"name": "owner-machine",
	}, AuthHeaders(owner.AccessToken), &ownerMachine)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var adminMachine dto.CreateMachineResponse
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/machines", map[string]any{
		"name": "admin-machine",
	}, AuthHeaders(admin.AccessToken), &adminMachine)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var ownMachines dto.MachinesListResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/machines", nil, AuthHeaders(admin.AccessToken), &ownMachines)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Len(t, ownMachines.Machines, 1)
	require.Equal(t, adminMachine.Machine.ID, ownMachines.Machines[0].ID)

	var allMachines dto.MachinesListResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/machines?scope=all", nil, AuthHeaders(admin.AccessToken), &allMachines)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Len(t, allMachines.Machines, 2)
	require.ElementsMatch(
		t,
		[]string{ownerMachine.Machine.ID, adminMachine.Machine.ID},
		[]string{allMachines.Machines[0].ID, allMachines.Machines[1].ID},
	)
}

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

	runtime := StartRuntime(t)

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()

	var owner dto.AuthTokensResponse
	response := DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/register", map[string]any{
		"email":    "owner-machine-scope-forbidden@example.com",
		"password": TestUserPassword,
		"name":     "owner-scope-forbidden",
	}, nil, &owner)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var workspaces []dto.WorkspaceResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, AuthHeaders(owner.AccessToken), &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID

	inviteToken := "tok_machine_scope_member"
	_, err := runtime.Store.CreateWorkspaceInvite(
		runtime.Ctx,
		owner.User.ID,
		workspaceID,
		"member-machine-scope@example.com",
		secrets.HashToken(inviteToken),
		time.Now().UTC().Add(10*time.Minute),
	)
	require.NoError(t, err)

	var member dto.AuthTokensResponse
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/register", map[string]any{
		"email":    "member-machine-scope@example.com",
		"password": TestUserPassword,
		"name":     "member-scope",
	}, nil, &member)
	require.Equal(t, http.StatusOK, response.StatusCode)

	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/accept-invite", map[string]any{
		"token": inviteToken,
	}, AuthHeaders(member.AccessToken), nil)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var apiErr struct {
		Error string `json:"error"`
	}
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/machines?scope=all", nil, AuthHeaders(member.AccessToken), &apiErr)
	require.Equal(t, http.StatusForbidden, response.StatusCode)
	require.Equal(t, "forbidden", apiErr.Error)
}
