package forwarder

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

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/service/role/execution"
)

func TestRoleOperationGatewayPreflightProjectsTypedClaimsAndExpectsNoContent(t *testing.T) {
	var seenPath, seenWorkspace string
	var seenClaims map[string]any
	var seenBody map[string]any
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
		seenPath = request.URL.Path
		seenWorkspace = request.Header.Get("X-Workspace-Id")
		seenClaims = decodeClaims(t, strings.TrimPrefix(request.Header.Get("Authorization"), "Bearer "))
		require.NoError(t, json.NewDecoder(request.Body).Decode(&seenBody))
		w.WriteHeader(http.StatusNoContent)
	}))
	defer upstream.Close()

	gateway, err := NewRoleOperationGateway(Dependencies{BaseURL: upstream.URL, HTTPClient: upstream.Client()})
	require.NoError(t, err)
	err = gateway.Preflight(context.Background(), RoleOperationPrincipal{
		WorkspaceID: "wsp_1", Subject: "usr_1", OwnedAgentIDs: []string{"agt_1"},
	}, RoleOperationRequest{
		Apply: &RoleOperationApplyRequest{RoleID: "role_1", RoleVersionID: "rlv_1", AgentID: "agt_1"},
	})
	require.NoError(t, err)
	require.Equal(t, RoleOperationPreflightPath, seenPath)
	require.Equal(t, "wsp_1", seenWorkspace)
	require.Equal(t, "usr_1", seenClaims["sub"])
	require.Equal(t, RoleOperationInternalPurpose, seenClaims["agentteam_internal_purpose"])
	require.Equal(t, "wsp_1", seenClaims["agent_ownership_workspace_id"])
	require.Equal(t, []any{"agt_1"}, seenClaims["owned_agent_ids"])
	require.NotContains(t, seenBody, "workspaceId")
	require.NotContains(t, seenBody, "subject")
	require.NotContains(t, seenBody, "purpose")
	require.NotContains(t, seenBody, "ownedAgentIds")
}

func TestRoleOperationGatewayNonApplyRequestsProjectWorkspaceClaimWithoutOwnedAgents(t *testing.T) {
	for name, requests := range map[string]struct {
		preflight RoleOperationRequest
		execute   RoleOperationExecuteRequest
	}{
		"create": {
			preflight: RoleOperationRequest{
				Create: &RoleOperationCreateRequest{Name: "Reviewer", Definition: "Review changes"},
			},
			execute: RoleOperationExecuteRequest{
				OperationKey: "op_create", RequestDigest: strings.Repeat("a", 64),
				RoleOperationRequest: RoleOperationRequest{
					Create: &RoleOperationCreateRequest{Name: "Reviewer", Definition: "Review changes"},
				},
			},
		},
		"update": {
			preflight: RoleOperationRequest{
				Update: &RoleOperationUpdateRequest{
					RoleID: "role_1", ExpectedRoleVersionID: "rlv_1",
					Description: stringPointer("updated"),
				},
			},
			execute: RoleOperationExecuteRequest{
				OperationKey: "op_update", RequestDigest: strings.Repeat("b", 64),
				RoleOperationRequest: RoleOperationRequest{
					Update: &RoleOperationUpdateRequest{
						RoleID: "role_1", ExpectedRoleVersionID: "rlv_1",
						Description: stringPointer("updated"),
					},
				},
			},
		},
		"delete": {
			preflight: RoleOperationRequest{
				Delete: &RoleOperationDeleteRequest{RoleID: "role_1", ExpectedRoleVersionID: "rlv_1"},
			},
			execute: RoleOperationExecuteRequest{
				OperationKey: "op_delete", RequestDigest: strings.Repeat("c", 64),
				RoleOperationRequest: RoleOperationRequest{
					Delete: &RoleOperationDeleteRequest{RoleID: "role_1", ExpectedRoleVersionID: "rlv_1"},
				},
			},
		},
	} {
		t.Run(name, func(t *testing.T) {
			var seenClaims []map[string]any
			upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
				claims := decodeClaims(t, strings.TrimPrefix(request.Header.Get("Authorization"), "Bearer "))
				seenClaims = append(seenClaims, claims)
				require.Equal(t, request.Header.Get("X-Workspace-Id"), claims["agent_ownership_workspace_id"])
				require.NotContains(t, claims, "owned_agent_ids")
				require.NotContains(t, claims, "owned_agents")
				if request.URL.Path == RoleOperationPreflightPath {
					w.WriteHeader(http.StatusNoContent)
					return
				}
				_ = json.NewEncoder(w).Encode(RoleOperationResult{
					RoleID: "role_authority", RoleVersionID: "rlv_authority",
				})
			}))
			defer upstream.Close()

			gateway, err := NewRoleOperationGateway(Dependencies{
				BaseURL: upstream.URL, HTTPClient: upstream.Client(),
			})
			require.NoError(t, err)
			principal := RoleOperationPrincipal{WorkspaceID: "wsp_1", Subject: "usr_1"}
			require.NoError(t, gateway.Preflight(context.Background(), principal, requests.preflight))
			_, err = gateway.Execute(context.Background(), principal, requests.execute)
			require.NoError(t, err)
			require.Len(t, seenClaims, 2)
		})
	}
}

func TestRoleOperationGatewayExecuteRequiresStrictAuthorityResult(t *testing.T) {
	for name, response := range map[string]string{
		"empty body":           ``,
		"malformed":            `{`,
		"missing role":         `{"roleId":"","roleVersionId":"rlv_1"}`,
		"missing version":      `{"roleId":"role_1"}`,
		"unknown field":        `{"roleId":"role_1","roleVersionId":"rlv_1","extra":true}`,
		"trailing response":    `{"roleId":"role_1","roleVersionId":"rlv_1"} {}`,
		"duplicate role":       `{"roleId":"role_other","roleId":"role_1","roleVersionId":"rlv_1"}`,
		"duplicate version":    `{"roleId":"role_1","roleVersionId":"rlv_other","roleVersionId":"rlv_1"}`,
		"case variant role":    `{"roleId":"role_other","RoleId":"role_1","roleVersionId":"rlv_1"}`,
		"case variant version": `{"roleId":"role_1","roleVersionId":"rlv_other","RoleVersionId":"rlv_1"}`,
		"unicode fold version": `{"roleId":"role_1","roleVersionId":"rlv_other","roleVerſionId":"rlv_1"}`,
	} {
		t.Run(name, func(t *testing.T) {
			upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
				w.WriteHeader(http.StatusOK)
				_, _ = w.Write([]byte(response))
			}))
			defer upstream.Close()
			gateway, err := NewRoleOperationGateway(Dependencies{BaseURL: upstream.URL, HTTPClient: upstream.Client()})
			require.NoError(t, err)
			_, err = gateway.Execute(context.Background(), validCoreRolePrincipal(), validCoreRoleExecuteRequest())
			var applyErr *execution.ApplyError
			require.ErrorAs(t, err, &applyErr)
			require.Equal(t, execution.ApplyResponseLost, applyErr.Outcome)
		})
	}

	var seenPath string
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
		seenPath = request.URL.Path
		_ = json.NewEncoder(w).Encode(RoleOperationResult{RoleID: "role_authority", RoleVersionID: "rlv_authority"})
	}))
	defer upstream.Close()
	gateway, err := NewRoleOperationGateway(Dependencies{BaseURL: upstream.URL, HTTPClient: upstream.Client()})
	require.NoError(t, err)
	result, err := gateway.Execute(context.Background(), validCoreRolePrincipal(), validCoreRoleExecuteRequest())
	require.NoError(t, err)
	require.Equal(t, RoleOperationResult{RoleID: "role_authority", RoleVersionID: "rlv_authority"}, result)
	require.Equal(t, RoleOperationExecutePath, seenPath)
}

func TestRoleOperationGatewayRecoverRequiresStrictFoundMissUnion(t *testing.T) {
	for name, response := range map[string]string{
		"empty body":               ``,
		"malformed":                `{`,
		"missing found":            `{"roleId":"role_1","roleVersionId":"rlv_1"}`,
		"found missing role":       `{"found":true,"roleVersionId":"rlv_1"}`,
		"found missing version":    `{"found":true,"roleId":"role_1"}`,
		"miss with role":           `{"found":false,"roleId":"role_1"}`,
		"miss with empty role":     `{"found":false,"roleId":""}`,
		"unknown field":            `{"found":false,"extra":true}`,
		"trailing response":        `{"found":false} {}`,
		"duplicate found":          `{"found":true,"found":false}`,
		"duplicate role":           `{"found":true,"roleId":"role_other","roleId":"role_1","roleVersionId":"rlv_1"}`,
		"case variant version":     `{"found":true,"roleId":"role_1","roleVersionId":"rlv_other","RoleVersionId":"rlv_1"}`,
		"untrimmed stable role":    `{"found":true,"roleId":" role_1","roleVersionId":"rlv_1"}`,
		"untrimmed stable version": `{"found":true,"roleId":"role_1","roleVersionId":"rlv_1 "}`,
		"malformed stable role":    `{"found":true,"roleId":"???","roleVersionId":"rlv_1"}`,
		"wrong role type":          `{"found":true,"roleId":"rlv_1","roleVersionId":"rlv_1"}`,
		"wrong version type":       `{"found":true,"roleId":"role_1","roleVersionId":"role_1"}`,
	} {
		t.Run(name, func(t *testing.T) {
			upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
				require.Equal(t, RoleOperationRecoveryPath, request.URL.Path)
				w.WriteHeader(http.StatusOK)
				_, _ = w.Write([]byte(response))
			}))
			defer upstream.Close()
			gateway, err := NewRoleOperationGateway(Dependencies{
				BaseURL: upstream.URL, HTTPClient: upstream.Client(),
			})
			require.NoError(t, err)

			_, err = gateway.Recover(
				context.Background(), validCoreRolePrincipal(), validCoreRoleExecuteRequest(),
			)

			var applyErr *execution.ApplyError
			require.ErrorAs(t, err, &applyErr)
			require.Equal(t, execution.ApplyResponseLost, applyErr.Outcome)
		})
	}

	for name, response := range map[string]RoleOperationRecoveryResult{
		"miss":  {},
		"found": {Found: true, RoleID: "role_1", RoleVersionID: "rlv_1"},
	} {
		t.Run(name, func(t *testing.T) {
			upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
				require.Equal(t, RoleOperationRecoveryPath, request.URL.Path)
				if response.Found {
					_, _ = w.Write([]byte(`{"found":true,"roleId":"role_1","roleVersionId":"rlv_1"}`))
					return
				}
				_, _ = w.Write([]byte(`{"found":false}`))
			}))
			defer upstream.Close()
			gateway, err := NewRoleOperationGateway(Dependencies{
				BaseURL: upstream.URL, HTTPClient: upstream.Client(),
			})
			require.NoError(t, err)

			got, err := gateway.Recover(
				context.Background(), validCoreRolePrincipal(), validCoreRoleExecuteRequest(),
			)

			require.NoError(t, err)
			require.Equal(t, response, got)
		})
	}
}

func TestRoleOperationGatewayRecoverOnlyClassifiesReceiptConflict(t *testing.T) {
	for _, testCase := range []struct {
		name          string
		status        int
		code          string
		deterministic bool
	}{
		{"operation conflict", http.StatusConflict, "role_operation_conflict", true},
		{"unauthorized", http.StatusUnauthorized, "authority_denied", false},
		{"forbidden", http.StatusForbidden, "authority_denied", false},
		{"missing", http.StatusNotFound, "not_found", false},
		{"invalid", http.StatusBadRequest, "invalid_request", false},
		{"server", http.StatusInternalServerError, "internal_error", false},
	} {
		t.Run(testCase.name, func(t *testing.T) {
			upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
				w.WriteHeader(testCase.status)
				_ = json.NewEncoder(w).Encode(map[string]string{
					"error": testCase.code, "message": "failed",
				})
			}))
			defer upstream.Close()
			gateway, err := NewRoleOperationGateway(Dependencies{
				BaseURL: upstream.URL, HTTPClient: upstream.Client(),
			})
			require.NoError(t, err)

			_, err = gateway.Recover(
				context.Background(), validCoreRolePrincipal(), validCoreRoleExecuteRequest(),
			)

			if testCase.deterministic {
				var deterministic *DeterministicRoleOperationError
				require.ErrorAs(t, err, &deterministic)
				require.Equal(t, testCase.code, deterministic.Code)
				return
			}
			var applyErr *execution.ApplyError
			require.ErrorAs(t, err, &applyErr)
			require.Equal(t, execution.ApplyResponseLost, applyErr.Outcome)
		})
	}
}

func TestRoleOperationGatewayClassifiesOnlyWhitelistedCodesDeterministically(t *testing.T) {
	for _, testCase := range []struct {
		name          string
		status        int
		code          string
		deterministic bool
	}{
		{"invalid", http.StatusBadRequest, "invalid_request", true},
		{"authority", http.StatusForbidden, "authority_denied", true},
		{"missing", http.StatusNotFound, "not_found", true},
		{"version conflict", http.StatusConflict, "role_version_conflict", true},
		{"operation conflict", http.StatusConflict, "role_operation_conflict", true},
		{"dependency unavailable", http.StatusServiceUnavailable, "role_dependency_unavailable", false},
		{"internal", http.StatusInternalServerError, "internal_error", false},
		{"unknown 404", http.StatusNotFound, "unknown_code", false},
		{"missing code 409", http.StatusConflict, "", false},
		{"malformed non-2xx", http.StatusConflict, "{", false},
	} {
		t.Run(testCase.name, func(t *testing.T) {
			upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
				w.WriteHeader(testCase.status)
				if testCase.code == "{" {
					_, _ = w.Write([]byte(`{`))
					return
				}
				if testCase.code == "" {
					_, _ = w.Write([]byte(`{"message":"failed"}`))
					return
				}
				_ = json.NewEncoder(w).Encode(map[string]string{"error": testCase.code, "message": "failed"})
			}))
			defer upstream.Close()
			gateway, err := NewRoleOperationGateway(Dependencies{BaseURL: upstream.URL, HTTPClient: upstream.Client()})
			require.NoError(t, err)
			_, err = gateway.Execute(context.Background(), validCoreRolePrincipal(), validCoreRoleExecuteRequest())
			if testCase.deterministic {
				var deterministic *DeterministicRoleOperationError
				require.ErrorAs(t, err, &deterministic)
				require.Equal(t, testCase.code, deterministic.Code)
				return
			}
			var applyErr *execution.ApplyError
			require.ErrorAs(t, err, &applyErr)
			require.Equal(t, execution.ApplyResponseLost, applyErr.Outcome)
		})
	}
}

func TestRoleOperationGatewayNetworkFailureIsResponseLost(t *testing.T) {
	gateway, err := NewRoleOperationGateway(Dependencies{
		BaseURL: "http://127.0.0.1:1",
		HTTPClient: &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
			return nil, errors.New("network down")
		})},
	})
	require.NoError(t, err)
	_, err = gateway.Execute(context.Background(), validCoreRolePrincipal(), validCoreRoleExecuteRequest())
	var applyErr *execution.ApplyError
	require.ErrorAs(t, err, &applyErr)
	require.Equal(t, execution.ApplyResponseLost, applyErr.Outcome)
}

func TestRoleOperationGatewayRejectsMalformedStableErrorEnvelope(t *testing.T) {
	for name, body := range map[string]string{
		"missing message":      `{"error":"not_found"}`,
		"legacy code":          `{"code":"not_found","message":"failed"}`,
		"unknown field":        `{"error":"not_found","message":"failed","extra":true}`,
		"duplicate error":      `{"error":"unknown_code","error":"not_found","message":"failed"}`,
		"duplicate message":    `{"error":"not_found","message":"ambiguous","message":"failed"}`,
		"case variant error":   `{"error":"unknown_code","Error":"not_found","message":"failed"}`,
		"case variant message": `{"error":"not_found","message":"ambiguous","Message":"failed"}`,
		"unicode fold message": `{"error":"not_found","message":"ambiguous","meſsage":"failed"}`,
	} {
		t.Run(name, func(t *testing.T) {
			upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
				w.WriteHeader(http.StatusNotFound)
				_, _ = w.Write([]byte(body))
			}))
			defer upstream.Close()
			gateway, err := NewRoleOperationGateway(Dependencies{BaseURL: upstream.URL, HTTPClient: upstream.Client()})
			require.NoError(t, err)

			_, err = gateway.Execute(context.Background(), validCoreRolePrincipal(), validCoreRoleExecuteRequest())
			var applyErr *execution.ApplyError
			require.ErrorAs(t, err, &applyErr)
			require.Equal(t, execution.ApplyResponseLost, applyErr.Outcome)
		})
	}
}

func TestRoleOperationGatewayRejectsIncompletePrincipalWithoutCallingUpstream(t *testing.T) {
	called := false
	upstream := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
		called = true
	}))
	defer upstream.Close()
	gateway, err := NewRoleOperationGateway(Dependencies{BaseURL: upstream.URL, HTTPClient: upstream.Client()})
	require.NoError(t, err)

	for _, principal := range []RoleOperationPrincipal{
		{Subject: "usr_1"},
		{WorkspaceID: "wsp_1"},
	} {
		_, err := gateway.Execute(context.Background(), principal, validCoreRoleExecuteRequest())
		var deterministic *DeterministicRoleOperationError
		require.ErrorAs(t, err, &deterministic)
		require.Equal(t, "authority_denied", deterministic.Code)
	}
	require.False(t, called)
}

func validCoreRolePrincipal() RoleOperationPrincipal {
	return RoleOperationPrincipal{
		WorkspaceID: "wsp_1", Subject: "usr_1", OwnedAgentIDs: []string{"agt_1"},
	}
}

func stringPointer(value string) *string {
	return &value
}

func validCoreRoleExecuteRequest() RoleOperationExecuteRequest {
	return RoleOperationExecuteRequest{
		OperationKey:  "op_1",
		RequestDigest: strings.Repeat("a", 64),
		RoleOperationRequest: RoleOperationRequest{
			Create: &RoleOperationCreateRequest{Name: "Reviewer", Definition: "Review changes"},
		},
	}
}

type roundTripFunc func(*http.Request) (*http.Response, error)

func (function roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
	return function(request)
}
