package api

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

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

	"github.com/tidbcloud/AgentTeam/internal/store/identity"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
	"github.com/tidbcloud/AgentTeam/internal/workspace/owner"
)

type fakeMiddlewareStore struct {
	authenticated identity.AuthenticatedUser
	authErr       error
	workspace     workspace.Summary
	workspaceErr  error
	authToken     string
	workspaceUser string
	workspaceID   string
}

func (fake *fakeMiddlewareStore) AuthenticateSession(_ context.Context, token string) (identity.AuthenticatedUser, error) {
	fake.authToken = token
	if fake.authErr != nil {
		return identity.AuthenticatedUser{}, fake.authErr
	}
	return fake.authenticated, nil
}

func (fake *fakeMiddlewareStore) GetWorkspaceForUser(_ context.Context, userID string, workspaceID string) (workspace.Summary, error) {
	fake.workspaceUser = userID
	fake.workspaceID = workspaceID
	if fake.workspaceErr != nil {
		return workspace.Summary{}, fake.workspaceErr
	}
	return fake.workspace, nil
}

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

	fake := &fakeMiddlewareStore{
		authenticated: identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}},
		workspace:     workspace.Summary{WorkspaceID: "wsp_1"},
	}
	router := gin.New()
	router.GET(
		"/protected",
		RequireAuthenticatedUser(fake),
		RequireVerifiedIdentity(false),
		RequireCurrentWorkspace(fake),
		func(ctx *gin.Context) {
			authenticated, authOK := AuthenticatedUserFromContext(ctx)
			workspaceSummary, workspaceOK := CurrentWorkspaceFromContext(ctx)
			ctx.JSON(http.StatusOK, gin.H{
				"authOK":      authOK,
				"workspaceOK": workspaceOK,
				"userId":      authenticated.User.ID,
				"workspaceId": workspaceSummary.WorkspaceID,
			})
		},
	)

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/protected", nil)
	request.Header.Set("Authorization", "Bearer session-token")
	request.Header.Set(HeaderWorkspaceID, "wsp_1")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "session-token", fake.authToken)
	require.Equal(t, "usr_1", fake.workspaceUser)
	require.Equal(t, "wsp_1", fake.workspaceID)

	var payload map[string]any
	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &payload))
	require.Equal(t, true, payload["authOK"])
	require.Equal(t, true, payload["workspaceOK"])
	require.Equal(t, "usr_1", payload["userId"])
	require.Equal(t, "wsp_1", payload["workspaceId"])
}

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

	fake := &fakeMiddlewareStore{
		authenticated: identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}},
	}
	router := gin.New()
	router.GET(
		"/protected",
		RequireAuthenticatedUser(fake),
		RequireVerifiedIdentity(true),
		func(ctx *gin.Context) {
			ctx.Status(http.StatusNoContent)
		},
	)

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/protected", nil)
	request.Header.Set("Authorization", "Bearer session-token")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusForbidden, recorder.Code)
	require.Contains(t, recorder.Body.String(), errorCodeEmailNotVerified)
}

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

	now := time.Now()
	fake := &fakeMiddlewareStore{
		authenticated: identity.AuthenticatedUser{User: identity.User{ID: "usr_1", EmailVerifiedAt: &now}},
	}
	router := gin.New()
	router.GET(
		"/protected",
		RequireAuthenticatedUser(fake),
		RequireVerifiedIdentity(true),
		func(ctx *gin.Context) {
			ctx.Status(http.StatusNoContent)
		},
	)

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/protected", nil)
	request.Header.Set("Authorization", "Bearer session-token")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusNoContent, recorder.Code)
}

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

	router := gin.New()
	router.GET("/ops", func(ctx *gin.Context) {
		SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_member"}})
	}, RequirePlatformOperator(), func(ctx *gin.Context) {
		ctx.Status(http.StatusNoContent)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/ops", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusForbidden, recorder.Code)
}

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

	router := gin.New()
	router.GET("/ops", func(ctx *gin.Context) {
		SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_admin", IsAdmin: true}})
	}, RequirePlatformOperator(), func(ctx *gin.Context) {
		ctx.Status(http.StatusNoContent)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/ops", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusNoContent, recorder.Code)
}

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

	fake := &fakeMiddlewareStore{
		authenticated: identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}},
	}
	router := gin.New()
	router.GET(
		"/protected",
		RequireAuthenticatedUser(fake),
		RequireVerifiedIdentity(false),
		RequireCurrentWorkspace(fake),
		func(ctx *gin.Context) {
			ctx.Status(http.StatusNoContent)
		},
	)

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/protected", nil)
	request.Header.Set("Authorization", "Bearer session-token")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusBadRequest, recorder.Code)
	require.Contains(t, recorder.Body.String(), "No workspace selected")
}

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

	fake := &fakeMiddlewareStore{
		authenticated: identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}},
		workspace:     workspace.Summary{WorkspaceID: "wsp_other"},
	}
	router := gin.New()
	router.GET(
		"/protected",
		RequireAuthenticatedUser(fake),
		RequireCurrentWorkspace(NewOwnedWorkspaceResolver(fake, owner.NewStaticSet([]string{"wsp_owned"}))),
		func(ctx *gin.Context) {
			ctx.Status(http.StatusNoContent)
		},
	)

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/protected", nil)
	request.Header.Set("Authorization", "Bearer session-token")
	request.Header.Set(HeaderWorkspaceID, "wsp_other")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusMisdirectedRequest, recorder.Code)
	require.Contains(t, recorder.Body.String(), "workspace_owner_mismatch")
}

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

	for _, test := range []struct {
		name          string
		path          string
		currentHeader string
		routeHeader   string
		resolvedID    string
		want          int
	}{
		{name: "path only", path: "/api/v1/workspaces/wsp_owned/kb", want: http.StatusNoContent},
		{name: "matching headers", path: "/api/v1/workspaces/wsp_owned/kb", currentHeader: "wsp_owned", routeHeader: "wsp_owned", want: http.StatusNoContent},
		{name: "current header conflict", path: "/api/v1/workspaces/wsp_owned/kb", currentHeader: "wsp_other", want: http.StatusBadRequest},
		{name: "route header conflict", path: "/api/v1/workspaces/wsp_owned/kb", routeHeader: "wsp_other", want: http.StatusBadRequest},
		{name: "escaped segment", path: "/api/v1/workspaces/%77sp_owned/kb", want: http.StatusBadRequest},
		{name: "malformed segment", path: "/api/v1/workspaces/wsp%20owned/kb", want: http.StatusBadRequest},
		{name: "resolver workspace conflict", path: "/api/v1/workspaces/wsp_owned/kb", resolvedID: "wsp_other", want: http.StatusBadRequest},
	} {
		t.Run(test.name, func(t *testing.T) {
			resolvedID := test.resolvedID
			if resolvedID == "" {
				resolvedID = "wsp_owned"
			}
			fake := &fakeMiddlewareStore{
				authenticated: identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}},
				workspace:     workspace.Summary{WorkspaceID: resolvedID},
			}
			router := gin.New()
			router.GET(
				"/api/v1/workspaces/:workspaceId/kb",
				RequireAuthenticatedUser(fake),
				RequireRouteWorkspace(NewOwnedWorkspaceResolver(fake, owner.NewStaticSet([]string{"wsp_owned", "wsp_other"}))),
				func(ctx *gin.Context) { ctx.Status(http.StatusNoContent) },
			)

			request := httptest.NewRequest(http.MethodGet, test.path, nil)
			request.Header.Set("Authorization", "Bearer session-token")
			if test.currentHeader != "" {
				request.Header.Set(HeaderWorkspaceID, test.currentHeader)
			}
			if test.routeHeader != "" {
				request.Header.Set(HeaderRouteWorkspaceID, test.routeHeader)
			}
			recorder := httptest.NewRecorder()
			router.ServeHTTP(recorder, request)

			require.Equal(t, test.want, recorder.Code, recorder.Body.String())
		})
	}
}

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

	recorder := httptest.NewRecorder()
	_, router := gin.CreateTestContext(recorder)
	var captured string
	router.GET("/test", func(ctx *gin.Context) {
		token, ok := BearerToken(ctx)
		captured = token
		ctx.JSON(http.StatusOK, gin.H{"ok": ok})
	})

	request := httptest.NewRequest(http.MethodGet, "/test", nil)
	request.Header.Set("Authorization", "Bearer my-token")
	router.ServeHTTP(recorder, request)

	require.Equal(t, "my-token", captured)
}

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

	recorder := httptest.NewRecorder()
	_, router := gin.CreateTestContext(recorder)
	var found bool
	router.GET("/test", func(ctx *gin.Context) {
		_, found = BearerToken(ctx)
	})

	request := httptest.NewRequest(http.MethodGet, "/test", nil)
	router.ServeHTTP(recorder, request)

	require.False(t, found)
}

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

	for _, header := range []string{"Basic abc", "Bearer", "Bearer  ", ""} {
		t.Run("header="+header, func(t *testing.T) {
			t.Parallel()

			recorder := httptest.NewRecorder()
			_, router := gin.CreateTestContext(recorder)
			var found bool
			router.GET("/test", func(ctx *gin.Context) {
				_, found = BearerToken(ctx)
			})

			request := httptest.NewRequest(http.MethodGet, "/test", nil)
			if header != "" {
				request.Header.Set("Authorization", header)
			}
			router.ServeHTTP(recorder, request)

			require.False(t, found)
		})
	}
}

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

	recorder := httptest.NewRecorder()
	_, router := gin.CreateTestContext(recorder)
	var captured string
	var found bool
	router.GET("/test", func(ctx *gin.Context) {
		captured, found = ProxyToken(ctx)
	})

	request := httptest.NewRequest(http.MethodGet, "/test", nil)
	request.Header.Set("Authorization", "Bearer my-token")
	router.ServeHTTP(recorder, request)

	require.True(t, found)
	require.Equal(t, "my-token", captured)
}

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

	for _, tc := range []struct {
		name   string
		header string
		value  string
	}{
		{name: "x-api-key", header: "x-api-key", value: "proxy-token"},
		{name: "api-key", header: "api-key", value: "proxy-token"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			recorder := httptest.NewRecorder()
			_, router := gin.CreateTestContext(recorder)
			var captured string
			var found bool
			router.GET("/test", func(ctx *gin.Context) {
				captured, found = ProxyToken(ctx)
			})

			request := httptest.NewRequest(http.MethodGet, "/test", nil)
			request.Header.Set(tc.header, tc.value)
			router.ServeHTTP(recorder, request)

			require.True(t, found)
			require.Equal(t, tc.value, captured)
		})
	}
}

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

	recorder := httptest.NewRecorder()
	_, router := gin.CreateTestContext(recorder)
	var found bool
	router.GET("/test", func(ctx *gin.Context) {
		_, found = ProxyToken(ctx)
	})

	request := httptest.NewRequest(http.MethodGet, "/test", nil)
	router.ServeHTTP(recorder, request)

	require.False(t, found)
}

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

	recorder := httptest.NewRecorder()
	_, router := gin.CreateTestContext(recorder)
	var captured string
	router.GET("/test", func(ctx *gin.Context) {
		id, ok := WorkspaceID(ctx)
		captured = id
		ctx.JSON(http.StatusOK, gin.H{"ok": ok})
	})

	request := httptest.NewRequest(http.MethodGet, "/test", nil)
	request.Header.Set(HeaderWorkspaceID, "wsp_123")
	router.ServeHTTP(recorder, request)

	require.Equal(t, "wsp_123", captured)
}

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

	recorder := httptest.NewRecorder()
	_, router := gin.CreateTestContext(recorder)
	var found bool
	router.GET("/test", func(ctx *gin.Context) {
		_, found = WorkspaceID(ctx)
	})

	request := httptest.NewRequest(http.MethodGet, "/test", nil)
	router.ServeHTTP(recorder, request)

	require.False(t, found)
}

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

	recorder := httptest.NewRecorder()
	_, router := gin.CreateTestContext(recorder)
	var captured string
	router.GET("/test", func(ctx *gin.Context) {
		id, ok := WorkspaceID(ctx)
		captured = id
		ctx.JSON(http.StatusOK, gin.H{"ok": ok})
	})

	request := httptest.NewRequest(http.MethodGet, "/test", nil)
	request.Header.Set(HeaderWorkspaceID, "  wsp_1  ")
	router.ServeHTTP(recorder, request)

	require.Equal(t, "wsp_1", captured)
}

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

	fake := &fakeMiddlewareStore{authErr: storeerr.ErrUnauthorized}
	router := gin.New()
	router.GET("/protected", RequireAuthenticatedUser(fake), func(ctx *gin.Context) {
		ctx.Status(http.StatusNoContent)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/protected", nil)
	request.Header.Set("Authorization", "Bearer bad-token")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusUnauthorized, recorder.Code)
	require.Contains(t, recorder.Body.String(), "Invalid access token")
}

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

	fake := &fakeMiddlewareStore{}
	router := gin.New()
	router.GET("/protected", RequireAuthenticatedUser(fake), func(ctx *gin.Context) {
		ctx.Status(http.StatusNoContent)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/protected", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusUnauthorized, recorder.Code)
	require.Contains(t, recorder.Body.String(), "Missing bearer token")
}

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

	fake := &fakeMiddlewareStore{
		authenticated: identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}},
		workspaceErr:  storeerr.ErrForbidden,
	}
	router := gin.New()
	router.GET("/protected", RequireAuthenticatedUser(fake), RequireVerifiedIdentity(false), RequireCurrentWorkspace(fake), func(ctx *gin.Context) {
		ctx.Status(http.StatusNoContent)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/protected", nil)
	request.Header.Set("Authorization", "Bearer token")
	request.Header.Set(HeaderWorkspaceID, "wsp_1")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusForbidden, recorder.Code)
	require.Contains(t, recorder.Body.String(), "Forbidden")
}

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

	fake := &fakeMiddlewareStore{workspace: workspace.Summary{WorkspaceID: "wsp_1"}}
	router := gin.New()
	router.GET("/protected", RequireCurrentWorkspace(fake), func(ctx *gin.Context) {
		ctx.Status(http.StatusNoContent)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/protected", nil)
	request.Header.Set(HeaderWorkspaceID, "wsp_1")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusUnauthorized, recorder.Code)
	require.Contains(t, recorder.Body.String(), "Unauthorized")
}

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

	fake := &fakeMiddlewareStore{
		authenticated: identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}},
	}
	router := gin.New()
	router.GET("/protected", RequireAuthenticatedUser(fake), RequireVerifiedIdentity(false), func(ctx *gin.Context) {
		ctx.Status(http.StatusNoContent)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/protected", nil)
	request.Header.Set("Authorization", "Bearer token")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusNoContent, recorder.Code)
}

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

	recorder := httptest.NewRecorder()
	_, router := gin.CreateTestContext(recorder)
	var found bool
	router.GET("/test", func(ctx *gin.Context) {
		_, found = AuthenticatedUserFromContext(ctx)
	})

	request := httptest.NewRequest(http.MethodGet, "/test", nil)
	router.ServeHTTP(recorder, request)

	require.False(t, found)
}

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

	recorder := httptest.NewRecorder()
	_, router := gin.CreateTestContext(recorder)
	var found bool
	router.GET("/test", func(ctx *gin.Context) {
		_, found = CurrentWorkspaceFromContext(ctx)
	})

	request := httptest.NewRequest(http.MethodGet, "/test", nil)
	router.ServeHTTP(recorder, request)

	require.False(t, found)
}
