package agent

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
	"testing"
	"time"

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

	"github.com/tidbcloud/AgentTeam/internal/schedule/manage"
	storeagent "github.com/tidbcloud/AgentTeam/internal/store/agent"
)

func TestAgentProjectListPagesFollowUpAndFinalPage(t *testing.T) {
	updatedAt := time.Date(2026, 8, 21, 13, 0, 0, 0, time.UTC)
	store := &fakeAgentProjectStore{listPage: storeagent.ProjectPage{
		Projects: []storeagent.Project{{ID: "prj_1", UpdatedAt: updatedAt}}, HasMore: true,
	}}
	router := projectTestRouter(fakeAgentAuthenticator{agent: storeagent.AuthenticatedAgent{
		AgentID: "agt_0000000000test", WorkspaceID: "wsp_test", MemberID: "member_1", MachineID: "machine_1",
	}}, store)

	first := agentTaskRequest(t, router, http.MethodGet, "/projects?limit=1", nil)
	require.Equal(t, http.StatusOK, first.Code, first.Body.String())
	cursor := responseJSONField(t, first.Body.Bytes(), "nextCursor")
	require.NotEmpty(t, cursor)
	require.True(t, responseJSONBool(t, first.Body.Bytes(), "hasMore"))

	second := agentTaskRequest(t, router, http.MethodGet, "/projects?limit=1&cursor="+url.QueryEscape(cursor), nil)
	require.Equal(t, http.StatusOK, second.Code, second.Body.String())
	require.Equal(t, "prj_1", store.listOptions.CursorID)
	require.Equal(t, updatedAt, store.listOptions.CursorUpdatedAt)

	store.listPage = storeagent.ProjectPage{Projects: []storeagent.Project{}, HasMore: false}
	final := agentTaskRequest(t, router, http.MethodGet, "/projects?limit=1", nil)
	require.Equal(t, http.StatusOK, final.Code)
	require.Empty(t, responseJSONField(t, final.Body.Bytes(), "nextCursor"))
	require.False(t, responseJSONBool(t, final.Body.Bytes(), "hasMore"))
}

func TestAgentProjectListRejectsInvalidCursorAndReturnsErrorEnvelope(t *testing.T) {
	store := &fakeAgentProjectStore{}
	router := projectTestRouter(fakeAgentAuthenticator{agent: storeagent.AuthenticatedAgent{
		AgentID: "agt_0000000000test", WorkspaceID: "wsp_test", MemberID: "member_1", MachineID: "machine_1",
	}}, store)
	invalid := agentTaskRequest(t, router, http.MethodGet, "/projects?cursor=not-base64", nil)
	require.Equal(t, http.StatusBadRequest, invalid.Code)
	require.Contains(t, invalid.Body.String(), "invalid project cursor")

	store.listErr = errors.New("project backend unavailable")
	failed := agentTaskRequest(t, router, http.MethodGet, "/projects", nil)
	require.Equal(t, http.StatusInternalServerError, failed.Code)
	require.Contains(t, failed.Body.String(), `"error"`)
}

func TestAgentTeamListPagesFilterBoundCursorAndFinalPage(t *testing.T) {
	store := &fakeAgentTeamStore{listPage: AgentTeamListPage{
		Teams: []AgentTeamSummary{{TeamID: "team_1", DisplayName: "Backend", Handle: "backend"}}, HasMore: true,
	}}
	router := agentTeamTestRouter(store, nil)
	first := agentTaskRequest(t, router, http.MethodGet, "/teams?q=OPS&limit=1", nil)
	require.Equal(t, http.StatusOK, first.Code, first.Body.String())
	cursor := responseJSONField(t, first.Body.Bytes(), "nextCursor")
	require.NotEmpty(t, cursor)
	mismatch := agentTaskRequest(t, router, http.MethodGet, "/teams?q=other&limit=1&cursor="+url.QueryEscape(cursor), nil)
	require.Equal(t, http.StatusBadRequest, mismatch.Code)
	require.Contains(t, mismatch.Body.String(), "invalid team cursor")

	second := agentTaskRequest(t, router, http.MethodGet, "/teams?q=ops&limit=1&cursor="+url.QueryEscape(cursor), nil)
	require.Equal(t, http.StatusOK, second.Code, second.Body.String())
	require.Equal(t, "ops", store.listOptions.Query)
	require.Equal(t, "team_1", store.listOptions.CursorTeamID)

	store.listPage = AgentTeamListPage{Teams: []AgentTeamSummary{}, HasMore: false}
	final := agentTaskRequest(t, router, http.MethodGet, "/teams?limit=1", nil)
	require.Equal(t, http.StatusOK, final.Code)
	require.False(t, responseJSONBool(t, final.Body.Bytes(), "hasMore"))
}

func TestAgentTeamListRejectsInvalidCursorAndReturnsErrorEnvelope(t *testing.T) {
	store := &fakeAgentTeamStore{}
	router := agentTeamTestRouter(store, nil)
	invalid := agentTaskRequest(t, router, http.MethodGet, "/teams?cursor=bad", nil)
	require.Equal(t, http.StatusBadRequest, invalid.Code)
	require.Contains(t, invalid.Body.String(), "invalid team cursor")

	store.listErr = errors.New("team backend unavailable")
	failed := agentTaskRequest(t, router, http.MethodGet, "/teams", nil)
	require.Equal(t, http.StatusInternalServerError, failed.Code)
	require.Contains(t, failed.Body.String(), `"error"`)
}

func TestAgentReviewListPagesFollowUpAndRejectsInvalidCursor(t *testing.T) {
	updatedAt := time.Date(2026, 8, 21, 13, 1, 0, 0, time.UTC)
	store := &fakeAgentReviewContextStore{page: storeagent.ReviewContextPage{
		Contexts: []storeagent.ReviewContext{{ID: "rcx_1", UpdatedAt: updatedAt}}, HasMore: true,
	}}
	router := reviewContextTestRouter(store)
	first := agentTaskRequest(t, router, http.MethodGet, "/threads/thread_1/review-contexts?limit=1", nil)
	require.Equal(t, http.StatusOK, first.Code, first.Body.String())
	cursor := responseJSONField(t, first.Body.Bytes(), "nextCursor")
	require.NotEmpty(t, cursor)

	second := agentTaskRequest(t, router, http.MethodGet, "/threads/thread_1/review-contexts?limit=1&cursor="+url.QueryEscape(cursor), nil)
	require.Equal(t, http.StatusOK, second.Code, second.Body.String())
	require.Equal(t, "rcx_1", store.options.CursorID)
	require.Equal(t, updatedAt, store.options.CursorUpdatedAt)

	invalid := agentTaskRequest(t, router, http.MethodGet, "/threads/thread_1/review-contexts?cursor=bad", nil)
	require.Equal(t, http.StatusBadRequest, invalid.Code)
	require.Contains(t, invalid.Body.String(), "invalid review context cursor")
}

type fakeAgentReviewContextStore struct {
	page         storeagent.ReviewContextPage
	options      storeagent.ReviewContextListOptions
	listErr      error
	threadAccess int
}

func (store *fakeAgentReviewContextStore) CreateReviewContextForAgent(context.Context, storeagent.AuthenticatedAgent, storeagent.UpsertReviewContextParams) (storeagent.ReviewContext, error) {
	return storeagent.ReviewContext{}, nil
}
func (store *fakeAgentReviewContextStore) GetReviewContext(context.Context, string, string) (*storeagent.ReviewContext, error) {
	return nil, nil
}
func (store *fakeAgentReviewContextStore) EnsureAgentThreadAccess(context.Context, string, string, string) error {
	store.threadAccess++
	return nil
}
func (store *fakeAgentReviewContextStore) EnsureAgentTaskAccess(context.Context, storeagent.AuthenticatedAgent, string) error {
	return nil
}
func (store *fakeAgentReviewContextStore) ListReviewContextsForThreadForAgent(context.Context, string, string) ([]storeagent.ReviewContext, error) {
	return store.page.Contexts, store.listErr
}
func (store *fakeAgentReviewContextStore) ListReviewContextsForThreadForAgentPage(_ context.Context, _ string, _ string, options storeagent.ReviewContextListOptions) (storeagent.ReviewContextPage, error) {
	store.options = options
	if store.listErr != nil {
		return storeagent.ReviewContextPage{}, store.listErr
	}
	return store.page, nil
}
func (store *fakeAgentReviewContextStore) ListReviewContextsForTaskForAgent(context.Context, storeagent.AuthenticatedAgent, string) ([]storeagent.ReviewContext, error) {
	return nil, nil
}
func (store *fakeAgentReviewContextStore) DeleteReviewContextForAgent(context.Context, storeagent.AuthenticatedAgent, string) error {
	return nil
}

func reviewContextTestRouter(store ReviewContextStore) *gin.Engine {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	RegisterRoutes(router, Dependencies{
		Auth:               fakeAgentAuthenticator{agent: storeagent.AuthenticatedAgent{AgentID: "agt_0000000000test", WorkspaceID: "wsp_test", MemberID: "member_1"}},
		ReviewContextStore: store,
	})
	return router
}

func testScheduleRunRouter(handler *ScheduleHandler) *gin.Engine {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.GET("/api/v1/workspaces/:workspaceId/agents/:agentId/runtime/schedules/:scheduleId/runs", handler.ListRuns)
	return router
}

func TestAgentScheduleRunListPagesFollowUpAndErrorEnvelope(t *testing.T) {
	attemptedAt := time.Date(2026, 8, 21, 13, 2, 0, 0, time.UTC)
	service := &fakeScheduleControlService{runPage: manage.RunPage{
		Runs: []manage.AutomationRunView{{ID: "run_1", AttemptedAt: attemptedAt}}, HasMore: true,
	}}
	handler := &ScheduleHandler{
		auth:   fakeAgentAuthenticator{agent: storeagent.AuthenticatedAgent{AgentID: "agt_0000000000test", WorkspaceID: "wsp_test"}},
		owners: &fakeTriggerOwnerStore{ownerUserID: "usr_1"}, schedules: service,
	}
	router := testScheduleRunRouter(handler)
	first := agentTaskRequest(t, router, http.MethodGet, "/schedules/sch_1/runs?limit=1", nil)
	require.Equal(t, http.StatusOK, first.Code, first.Body.String())
	cursor := responseJSONField(t, first.Body.Bytes(), "nextCursor")
	require.NotEmpty(t, cursor)

	second := agentTaskRequest(t, router, http.MethodGet, "/schedules/sch_1/runs?limit=1&cursor="+url.QueryEscape(cursor), nil)
	require.Equal(t, http.StatusOK, second.Code, second.Body.String())
	require.Equal(t, "run_1", service.runOptions.CursorID)
	require.Equal(t, attemptedAt, *service.runOptions.CursorAttempted)

	invalid := agentTaskRequest(t, router, http.MethodGet, "/schedules/sch_1/runs?cursor=bad", nil)
	require.Equal(t, http.StatusBadRequest, invalid.Code)
	require.Contains(t, invalid.Body.String(), "invalid scheduled-message run cursor")

	service.runErr = errors.New("run backend unavailable")
	failed := agentTaskRequest(t, router, http.MethodGet, "/schedules/sch_1/runs", nil)
	require.Equal(t, http.StatusInternalServerError, failed.Code)
	require.Contains(t, failed.Body.String(), `"error"`)
}

func TestAgentScheduleRunListTraversesEffectiveUnattemptedCursor(t *testing.T) {
	times := []time.Time{
		time.Date(2026, 8, 21, 13, 10, 0, 0, time.UTC),
		time.Date(2026, 8, 21, 13, 9, 0, 0, time.UTC),
		time.Date(2026, 8, 21, 13, 8, 0, 0, time.UTC),
		time.Date(2026, 8, 21, 13, 7, 0, 0, time.UTC),
		time.Date(2026, 8, 21, 13, 6, 0, 0, time.UTC),
	}
	service := &fakeScheduleControlService{runPages: []manage.RunPage{
		{Runs: []manage.AutomationRunView{{ID: "run_1", AttemptedAt: times[0]}, {ID: "run_2", AttemptedAt: times[1]}}, HasMore: true},
		{Runs: []manage.AutomationRunView{{ID: "run_3", AttemptedAt: times[2]}, {ID: "run_4", AttemptedAt: times[3]}}, HasMore: true},
		{Runs: []manage.AutomationRunView{{ID: "run_5", AttemptedAt: times[4]}}, HasMore: false},
		{Runs: []manage.AutomationRunView{}, HasMore: false},
	}}
	handler := &ScheduleHandler{
		auth:   fakeAgentAuthenticator{agent: storeagent.AuthenticatedAgent{AgentID: "agt_0000000000test", WorkspaceID: "wsp_test"}},
		owners: &fakeTriggerOwnerStore{ownerUserID: "usr_1"}, schedules: service,
	}
	router := testScheduleRunRouter(handler)

	seen := make([]string, 0, 5)
	requestPage := func(cursor string) string {
		path := "/schedules/sch_1/runs?limit=2"
		if cursor != "" {
			path += "&cursor=" + url.QueryEscape(cursor)
		}
		response := agentTaskRequest(t, router, http.MethodGet, path, nil)
		require.Equal(t, http.StatusOK, response.Code, response.Body.String())
		var payload struct {
			Runs       []manage.AutomationRunView `json:"runs"`
			HasMore    bool                       `json:"hasMore"`
			NextCursor string                     `json:"nextCursor"`
		}
		require.NoError(t, json.Unmarshal(response.Body.Bytes(), &payload))
		for _, run := range payload.Runs {
			seen = append(seen, run.ID)
		}
		return payload.NextCursor
	}

	cursor := requestPage("")
	require.NotEmpty(t, cursor)
	cursor = requestPage(cursor)
	require.NotEmpty(t, cursor)
	cursor = requestPage(cursor)
	require.Empty(t, cursor)
	require.Equal(t, []string{"run_1", "run_2", "run_3", "run_4", "run_5"}, seen)

	// Following the terminal tuple is a stable empty page. This mirrors a
	// client that persists the last effective key even though the terminal
	// response correctly omits nextCursor.
	terminalCursor, err := encodeAgentCollectionCursor(agentScheduleRunCursor{
		Version: 2, ScheduleID: "sch_1", AttemptedAt: times[4], ID: "run_5",
	})
	require.NoError(t, err)
	require.Empty(t, requestPage(terminalCursor))

	staleVersion, err := encodeAgentCollectionCursor(agentScheduleRunCursor{
		Version: 1, ScheduleID: "sch_1", AttemptedAt: times[4], ID: "run_5",
	})
	require.NoError(t, err)
	invalid := agentTaskRequest(t, router, http.MethodGet, "/schedules/sch_1/runs?cursor="+url.QueryEscape(staleVersion), nil)
	require.Equal(t, http.StatusBadRequest, invalid.Code)
	require.Contains(t, invalid.Body.String(), "invalid scheduled-message run cursor")
}

func responseJSONField(t *testing.T, raw []byte, key string) string {
	t.Helper()
	var payload map[string]any
	require.NoError(t, json.Unmarshal(raw, &payload))
	value, _ := payload[key].(string)
	return value
}

func responseJSONBool(t *testing.T, raw []byte, key string) bool {
	t.Helper()
	var payload map[string]any
	require.NoError(t, json.Unmarshal(raw, &payload))
	value, _ := payload[key].(bool)
	return value
}
