package forwarder

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

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

	"github.com/tidbcloud/AgentTeam/internal/api/agent"
	"github.com/tidbcloud/AgentTeam/internal/service/approvalapp"
	"github.com/tidbcloud/AgentTeam/internal/service/role/execution"
	storeagent "github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

func requireRoleReadImplemented(t *testing.T, err error) {
	t.Helper()
	require.NoError(t, err)
}

func TestRoleReadGatewayListUsesHumanSubjectWorkspaceAndStablePagination(t *testing.T) {
	var seenMethod, seenPath, seenWorkspace string
	var seenQuery map[string][]string
	var seenClaims map[string]any
	upstream := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		seenMethod, seenPath = request.Method, request.URL.Path
		seenWorkspace = request.Header.Get("X-Workspace-Id")
		seenQuery = request.URL.Query()
		seenClaims = decodeClaims(t, strings.TrimPrefix(request.Header.Get("Authorization"), "Bearer "))
		require.NotContains(t, seenClaims, "agentteam_internal_purpose")
		require.NotContains(t, seenClaims, "agent_ownership_workspace_id")
		require.NotContains(t, seenClaims, "owned_agent_ids")
		_ = json.NewEncoder(writer).Encode(map[string]any{
			"items": []map[string]any{{
				"id": "role_1", "name": "Reviewer", "description": "Reviews",
				"definition": "Review changes", "visibility": "workspace",
				"roleVersionId": "rlv_1", "canManage": true,
			}},
			"page": 2, "perPage": 7, "total": 15, "hasMore": true,
		})
	}))
	defer upstream.Close()

	gateway, err := NewRoleReadGateway(Dependencies{
		BaseURL: upstream.URL, HTTPClient: upstream.Client(),
	})
	require.NoError(t, err)
	result, err := gateway.List(context.Background(), execution.RoleReadPrincipal{
		WorkspaceID: "wsp_1", Subject: "usr_manager",
	}, execution.RoleListInput{
		View: "created_by_me", Query: "review", Limit: 7, Cursor: "2",
	})
	requireRoleReadImplemented(t, err)

	require.Equal(t, http.MethodGet, seenMethod)
	require.Equal(t, "/api/v1/roles", seenPath)
	require.Equal(t, "wsp_1", seenWorkspace)
	require.Equal(t, "usr_manager", seenClaims["sub"])
	require.Equal(t, "usr_manager", seenClaims["agentteam_user_id"])
	require.Equal(t, []string{"created_by_me"}, seenQuery["view"])
	require.Equal(t, []string{"review"}, seenQuery["q"])
	require.Equal(t, []string{"2"}, seenQuery["page"])
	require.Equal(t, []string{"7"}, seenQuery["per_page"])
	require.Equal(t, execution.RoleListResult{
		Roles: []execution.RoleSummary{{
			RoleID: "role_1", Name: "Reviewer", Description: "Reviews",
			Visibility: "workspace", RoleVersionID: "rlv_1", CanManage: true,
		}},
		NextCursor: "3",
	}, result)
}

func TestRoleReadGatewayGetEscapesRoleIDAndProjectsMinimalDetail(t *testing.T) {
	var seenPath, seenQuery string
	upstream := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		seenPath = request.URL.EscapedPath()
		seenQuery = request.URL.RawQuery
		_ = json.NewEncoder(writer).Encode(map[string]any{
			"id": "role_1", "name": "Reviewer", "description": "Reviews",
			"definition": "Review changes", "visibility": "private",
			"roleVersionId": "rlv_1", "canManage": false,
		})
	}))
	defer upstream.Close()

	gateway, err := NewRoleReadGateway(Dependencies{
		BaseURL: upstream.URL, HTTPClient: upstream.Client(),
	})
	require.NoError(t, err)
	result, err := gateway.Get(context.Background(), execution.RoleReadPrincipal{
		WorkspaceID: "wsp_1", Subject: "usr_manager",
	}, "role_1")
	requireRoleReadImplemented(t, err)
	require.Equal(t, "/api/v1/roles/role_1", seenPath)
	require.Empty(t, seenQuery)
	require.Equal(t, execution.RoleDetail{
		RoleID: "role_1", Name: "Reviewer", Description: "Reviews",
		Definition: "Review changes", Visibility: "private",
		RoleVersionID: "rlv_1", CanManage: false,
	}, result)
}

func TestRoleReadGatewayRejectsRoleIDOutsideOneSafePathSegmentThroughRuntime(t *testing.T) {
	for name, roleID := range map[string]string{
		"query":            "role_1?versionId=rlv_old",
		"fragment":         "role_1#fragment",
		"slash":            "role_1/child",
		"encoded slash":    "role_1%2fchild",
		"encoded query":    "role_1%3fversionId=rlv_old",
		"encoded fragment": "role_1%23fragment",
		"dot segment":      ".",
		"parent segment":   "..",
	} {
		t.Run(name, func(t *testing.T) {
			upstreamCalls := 0
			upstream := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
				upstreamCalls++
			}))
			defer upstream.Close()
			gateway, err := NewRoleReadGateway(Dependencies{
				BaseURL: upstream.URL, HTTPClient: upstream.Client(),
			})
			require.NoError(t, err)

			_, err = gateway.Get(context.Background(), execution.RoleReadPrincipal{
				WorkspaceID: "wsp_1", Subject: "usr_manager",
			}, roleID)
			require.True(t, execution.IsRoleReadInputError(err))

			payload, err := json.Marshal(map[string]string{"roleId": roleID})
			require.NoError(t, err)
			request := httptest.NewRequest(
				http.MethodPost,
				"/api/v1/workspaces/wsp_test/agents/agt_test/runtime/roles/get",
				bytes.NewReader(payload),
			)
			request.Header.Set("Authorization", "Bearer test-token")
			request.Header.Set("Content-Type", "application/json")
			response := httptest.NewRecorder()
			roleReadRuntimeRouter(gateway).ServeHTTP(response, request)
			require.Equal(t, http.StatusBadRequest, response.Code, response.Body.String())
			require.Contains(t, response.Body.String(), "role_invalid_request")
			require.Zero(t, upstreamCalls)
		})
	}
}

func TestRoleReadGatewayUsesBoundedOpaqueDecimalPageCursor(t *testing.T) {
	tests := []struct {
		name   string
		input  execution.RoleListInput
		status int
		body   string
	}{
		{
			name:  "empty cursor defaults page one and limit fifty",
			input: execution.RoleListInput{}, status: http.StatusOK,
			body: `{"items":[],"page":1,"perPage":50,"total":0,"hasMore":false}`,
		},
		{name: "zero cursor", input: execution.RoleListInput{Cursor: "0"}, status: http.StatusOK, body: `{}`},
		{name: "negative cursor", input: execution.RoleListInput{Cursor: "-1"}, status: http.StatusOK, body: `{}`},
		{name: "non decimal cursor", input: execution.RoleListInput{Cursor: "next"}, status: http.StatusOK, body: `{}`},
		{name: "limit above maximum", input: execution.RoleListInput{Limit: 101}, status: http.StatusOK, body: `{}`},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			upstream := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
				writer.WriteHeader(test.status)
				_, _ = writer.Write([]byte(test.body))
			}))
			defer upstream.Close()
			gateway, err := NewRoleReadGateway(Dependencies{
				BaseURL: upstream.URL, HTTPClient: upstream.Client(),
			})
			require.NoError(t, err)
			_, err = gateway.List(context.Background(), execution.RoleReadPrincipal{
				WorkspaceID: "wsp_1", Subject: "usr_manager",
			}, test.input)
			if test.name == "empty cursor defaults page one and limit fifty" {
				requireRoleReadImplemented(t, err)
				return
			}
			require.Error(t, err)
		})
	}
}

func TestRoleReadGatewayRejectsIncompleteOrAmbiguousResponses(t *testing.T) {
	for name, body := range map[string]string{
		"missing id":       `{"id":"","name":"Reviewer","definition":"Review","visibility":"private","roleVersionId":"rlv_1","canManage":true}`,
		"unknown field":    `{"id":"role_1","name":"Reviewer","definition":"Review","visibility":"private","roleVersionId":"rlv_1","canManage":true,"secret":"x"}`,
		"trailing value":   `{"id":"role_1","name":"Reviewer","definition":"Review","visibility":"private","roleVersionId":"rlv_1","canManage":true} {}`,
		"duplicate id":     `{"id":"role_other","id":"role_1","name":"Reviewer","definition":"Review","visibility":"private","roleVersionId":"rlv_1","canManage":true}`,
		"case variant id":  `{"id":"role_other","Id":"role_1","name":"Reviewer","definition":"Review","visibility":"private","roleVersionId":"rlv_1","canManage":true}`,
		"incomplete list":  `{"items":[],"page":1,"perPage":50,"total":0}`,
		"unknown list key": `{"items":[],"page":1,"perPage":50,"total":0,"hasMore":false,"secret":true}`,
	} {
		t.Run(name, func(t *testing.T) {
			upstream := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
				_, _ = writer.Write([]byte(body))
			}))
			defer upstream.Close()
			gateway, err := NewRoleReadGateway(Dependencies{
				BaseURL: upstream.URL, HTTPClient: upstream.Client(),
			})
			require.NoError(t, err)
			principal := execution.RoleReadPrincipal{WorkspaceID: "wsp_1", Subject: "usr_manager"}
			if strings.Contains(name, "list") {
				_, err = gateway.List(context.Background(), principal, execution.RoleListInput{})
			} else {
				_, err = gateway.Get(context.Background(), principal, "role_1")
			}
			require.Error(t, err)
		})
	}
}

func TestRoleReadGatewayInputErrorsMapThroughRuntimeWithoutUpstreamCall(t *testing.T) {
	tests := []struct {
		name  string
		query string
	}{
		{name: "zero cursor", query: "cursor=0"},
		{name: "negative cursor", query: "cursor=-1"},
		{name: "non decimal cursor", query: "cursor=next"},
		{name: "leading zero cursor", query: "cursor=01"},
		{name: "limit above maximum", query: "limit=101"},
		{name: "invalid view", query: "view=secret"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			upstreamCalls := 0
			upstream := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
				upstreamCalls++
			}))
			defer upstream.Close()
			gateway, err := NewRoleReadGateway(Dependencies{
				BaseURL: upstream.URL, HTTPClient: upstream.Client(),
			})
			require.NoError(t, err)

			response := roleReadRuntimeRequest(
				t, roleReadRuntimeRouter(gateway), "/roles?"+test.query,
			)
			require.Equal(t, http.StatusBadRequest, response.Code, response.Body.String())
			require.Contains(t, response.Body.String(), "role_invalid_request")
			require.Zero(t, upstreamCalls)
		})
	}
}

func TestRoleReadGatewayStrictResponseErrorStaysInternalThroughRuntime(t *testing.T) {
	for _, test := range []struct {
		name      string
		operation string
		body      string
	}{
		{
			name: "list unknown", operation: "list",
			body: `{"items":[],"page":1,"perPage":50,"total":0,"hasMore":false,"secret":true}`,
		},
		{
			name: "list incomplete", operation: "list",
			body: `{"items":[],"page":1,"perPage":50,"total":0}`,
		},
		{
			name: "get unknown", operation: "get",
			body: `{"id":"role_1","name":"Reviewer","definition":"Review","visibility":"private","roleVersionId":"rlv_1","canManage":true,"secret":true}`,
		},
		{
			name: "get incomplete", operation: "get",
			body: `{"id":"","name":"Reviewer","definition":"Review","visibility":"private","roleVersionId":"rlv_1","canManage":true}`,
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			upstream := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
				_, _ = writer.Write([]byte(test.body))
			}))
			defer upstream.Close()
			gateway, err := NewRoleReadGateway(Dependencies{
				BaseURL: upstream.URL, HTTPClient: upstream.Client(),
			})
			require.NoError(t, err)

			response := roleReadRuntimeOperationRequest(
				t, roleReadRuntimeRouter(gateway), test.operation,
			)
			require.Equal(t, http.StatusInternalServerError, response.Code, response.Body.String())
			require.Contains(t, response.Body.String(), "role_internal")
			require.NotContains(t, response.Body.String(), "secret")
		})
	}
}

func TestRoleReadGatewayUpstreamStatusMatrixThroughRuntime(t *testing.T) {
	for _, test := range []struct {
		name         string
		upstream     int
		status       int
		responseCode string
	}{
		{name: "bad request", upstream: http.StatusBadRequest, status: http.StatusInternalServerError, responseCode: "role_internal"},
		{name: "unauthorized", upstream: http.StatusUnauthorized, status: http.StatusInternalServerError, responseCode: "role_internal"},
		{name: "forbidden", upstream: http.StatusForbidden, status: http.StatusForbidden, responseCode: "role_forbidden"},
		{name: "not found", upstream: http.StatusNotFound, status: http.StatusNotFound, responseCode: "role_not_found"},
		{name: "conflict", upstream: http.StatusConflict, status: http.StatusConflict, responseCode: "role_conflict"},
		{name: "unprocessable", upstream: http.StatusUnprocessableEntity, status: http.StatusInternalServerError, responseCode: "role_internal"},
		{name: "internal", upstream: http.StatusInternalServerError, status: http.StatusInternalServerError, responseCode: "role_internal"},
	} {
		for _, operation := range []string{"list", "get"} {
			t.Run(test.name+"/"+operation, func(t *testing.T) {
				upstream := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
					writer.WriteHeader(test.upstream)
					_, _ = writer.Write([]byte(`{"error":"failed","message":"upstream secret"}`))
				}))
				defer upstream.Close()
				gateway, err := NewRoleReadGateway(Dependencies{
					BaseURL: upstream.URL, HTTPClient: upstream.Client(),
				})
				require.NoError(t, err)

				response := roleReadRuntimeOperationRequest(
					t, roleReadRuntimeRouter(gateway), operation,
				)
				require.Equal(t, test.status, response.Code, response.Body.String())
				require.Contains(t, response.Body.String(), test.responseCode)
				require.NotContains(t, response.Body.String(), "upstream secret")
			})
		}
	}
}

func TestRoleReadGatewayNetworkErrorStaysInternalThroughRuntime(t *testing.T) {
	for _, operation := range []string{"list", "get"} {
		t.Run(operation, func(t *testing.T) {
			gateway, err := NewRoleReadGateway(Dependencies{
				BaseURL: "http://127.0.0.1:1",
				HTTPClient: &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
					return nil, errors.New("network secret")
				})},
			})
			require.NoError(t, err)
			response := roleReadRuntimeOperationRequest(
				t, roleReadRuntimeRouter(gateway), operation,
			)
			require.Equal(t, http.StatusInternalServerError, response.Code, response.Body.String())
			require.Contains(t, response.Body.String(), "role_internal")
			require.NotContains(t, response.Body.String(), "network secret")
		})
	}
}

type roleReadRuntimeAuth struct{}

func (roleReadRuntimeAuth) AuthenticateAgent(
	_ context.Context,
	agentID, bearerToken string,
) (storeagent.AuthenticatedAgent, error) {
	if agentID != "agt_test" || bearerToken != "test-token" {
		return storeagent.AuthenticatedAgent{}, storeerr.ErrUnauthorized
	}
	return storeagent.AuthenticatedAgent{
		AgentID: "agt_test", WorkspaceID: "wsp_test",
		MemberID: "mem_test", MachineID: "mac_test",
	}, nil
}

type roleReadRuntimeManager struct{}

func (roleReadRuntimeManager) ResolveManager(
	context.Context,
	execution.AgentPrincipal,
) (execution.Manager, error) {
	return execution.Manager{UserID: "usr_manager", MemberID: "mem_manager"}, nil
}

type roleReadRuntimeRequests struct{}

func (roleReadRuntimeRequests) RequestGet(
	context.Context,
	execution.RequestGetCommand,
) (execution.RequestResult, error) {
	return execution.RequestResult{}, nil
}

type roleReadRuntimeApprovals struct{}

func (roleReadRuntimeApprovals) SubmitRequest(
	context.Context,
	string,
	approvalapp.SubmitCommand,
) (approvalapp.SubmitResult, error) {
	return approvalapp.SubmitResult{}, nil
}

func (roleReadRuntimeApprovals) PresentPending(
	context.Context,
	approvalapp.SubmitResult,
) (app.AppNotificationResult, error) {
	return app.AppNotificationResult{}, nil
}

func roleReadRuntimeRouter(reads *RoleReadGateway) *gin.Engine {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	agent.RegisterRoutes(router, agent.Dependencies{
		Auth:             roleReadRuntimeAuth{},
		RoleManagers:     roleReadRuntimeManager{},
		RoleReads:        reads,
		RoleRequests:     roleReadRuntimeRequests{},
		ApprovalSubjects: roleReadRuntimeApprovals{},
	})
	return router
}

func roleReadRuntimeRequest(
	t *testing.T,
	router http.Handler,
	suffix string,
) *httptest.ResponseRecorder {
	t.Helper()
	request := httptest.NewRequest(
		http.MethodGet,
		"/api/v1/workspaces/wsp_test/agents/agt_test/runtime"+suffix,
		bytes.NewReader(nil),
	)
	request.Header.Set("Authorization", "Bearer test-token")
	response := httptest.NewRecorder()
	router.ServeHTTP(response, request)
	return response
}

func roleReadRuntimeOperationRequest(
	t *testing.T,
	router http.Handler,
	operation string,
) *httptest.ResponseRecorder {
	t.Helper()
	if operation == "list" {
		return roleReadRuntimeRequest(t, router, "/roles")
	}
	request := httptest.NewRequest(
		http.MethodPost,
		"/api/v1/workspaces/wsp_test/agents/agt_test/runtime/roles/get",
		bytes.NewBufferString(`{"roleId":"role_1"}`),
	)
	request.Header.Set("Authorization", "Bearer test-token")
	request.Header.Set("Content-Type", "application/json")
	response := httptest.NewRecorder()
	router.ServeHTTP(response, request)
	return response
}
