package webagent

import (
	"errors"
	"net/http"
	"net/http/httptest"
	"testing"

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

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/runtimecontrol"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
)

// TestReviewDiffBatchBuildsResponseFromRuntimeResults is the webagent-side
// pin for the batch endpoint. It proves the handler:
//
//  1. Fans-in to a single Runtime.GetAgentReviewDiffBatch call (no
//     handler-level fan-out into N internal HTTP calls).
//  2. Forwards the raw path bytes verbatim — including leading/trailing
//     whitespace — into the Runtime call.
//  3. Builds the response with successful paths under diffs and per-path
//     errors under errors, so a single bad path does not break the batch.
func TestReviewDiffBatchBuildsResponseFromRuntimeResults(t *testing.T) {
	t.Parallel()

	store := &fakeStore{review: &agent.ReviewContext{
		ID: "rcx_1", MachineID: "mach_1", SourceAgentID: "agt_1", RootPath: "/repo",
		BaseRef: "main", HeadRef: "feature",
	}}
	runtime := &fakeRuntime{
		diff: agent.AgentReviewDiff{Status: "modified", Path: "foo.go"},
	}
	handler := &ReviewHandler{reviewStore: store, runtime: runtime}

	body := dto.ReviewDiffBatchRequest{
		Scope: "branch",
		Paths: []string{"foo.go", " bar.go"},
	}
	request := reviewDiffBatchRequest(t, body)

	response, err := handler.DiffBatch(request)
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, response.Status)
	// lastDiff captures the most recent path the runtime saw — we can't
	// observe every path in a single field, but we can assert the scope
	// and refs traveled with the call.
	require.Equal(t, "branch", runtime.lastDiff.Scope)
	require.Equal(t, "main", runtime.lastDiff.BaseRef)
	require.Equal(t, "feature", runtime.lastDiff.HeadRef)

	typed, ok := response.Body.(dto.ReviewDiffBatchResponse)
	require.True(t, ok, "response body MUST be a ReviewDiffBatchResponse, got %T", response.Body)
	require.Len(t, typed.Diffs, 2)
	require.Equal(t, "modified", typed.Diffs["foo.go"].Status)
	require.Equal(t, "modified", typed.Diffs[" bar.go"].Status,
		"fakeRuntime returns the same diff for every path — the point is the handler forwards both paths")
}

// TestReviewDiffBatchRejectsEmptyPathEntries pins input validation: an
// empty string in the paths slice MUST target as 400, not silently reach
// the Runtime.
func TestReviewDiffBatchRejectsEmptyPathEntries(t *testing.T) {
	t.Parallel()

	store := &fakeStore{review: &agent.ReviewContext{ID: "rcx_1", MachineID: "mach_1"}}
	runtime := &fakeRuntime{}
	handler := &ReviewHandler{reviewStore: store, runtime: runtime}

	body := dto.ReviewDiffBatchRequest{Scope: "branch", Paths: []string{"foo.go", ""}}
	_, err := handler.DiffBatch(reviewDiffBatchRequest(t, body))
	requireHTTPErrStatus(t, err, http.StatusBadRequest)
}

// TestReviewDiffBatchCapsPathList pins the bounded-batch contract.
func TestReviewDiffBatchCapsPathList(t *testing.T) {
	t.Parallel()

	store := &fakeStore{review: &agent.ReviewContext{ID: "rcx_1", MachineID: "mach_1"}}
	runtime := &fakeRuntime{}
	handler := &ReviewHandler{reviewStore: store, runtime: runtime}

	paths := make([]string, 65)
	for i := range paths {
		paths[i] = "p" + string(rune('a'+i%26)) + ".go"
	}
	body := dto.ReviewDiffBatchRequest{Scope: "branch", Paths: paths}
	_, err := handler.DiffBatch(reviewDiffBatchRequest(t, body))
	requireHTTPErrStatus(t, err, http.StatusBadRequest)
}

// TestReviewDiffBatchReturnsUnsupportedSentinelAs422 proves the top-level
// unsupported-sentinel path: when the Runtime returns
// ErrReviewTargetUnsupported for the batch, the handler returns 422 so the
// frontend can show the stable product-limitation message.
func TestReviewDiffBatchReturnsUnsupportedSentinelAs422(t *testing.T) {
	t.Parallel()

	store := &fakeStore{review: &agent.ReviewContext{ID: "rcx_1", MachineID: "mach_1"}}
	runtime := &fakeRuntime{diffBatchErr: runtimecontrol.ErrReviewTargetUnsupported}
	handler := &ReviewHandler{reviewStore: store, runtime: runtime}

	body := dto.ReviewDiffBatchRequest{Scope: "branch", Paths: []string{"foo.go"}}
	_, err := handler.DiffBatch(reviewDiffBatchRequest(t, body))
	requireHTTPErrStatus(t, err, http.StatusUnprocessableEntity)
}

// TestReviewDiffBatchPropagatesMachineOfflineAs503 proves the offline
// case still returns through the existing reviewRuntimeError mapping
// when the batch RPC hits it.
func TestReviewDiffBatchPropagatesMachineOfflineAs503(t *testing.T) {
	t.Parallel()

	store := &fakeStore{review: &agent.ReviewContext{ID: "rcx_1", MachineID: "mach_1"}}
	runtime := &fakeRuntime{diffBatchErr: runtimecontrol.ErrMachineOffline}
	handler := &ReviewHandler{reviewStore: store, runtime: runtime}

	body := dto.ReviewDiffBatchRequest{Scope: "branch", Paths: []string{"foo.go"}}
	_, err := handler.DiffBatch(reviewDiffBatchRequest(t, body))
	requireHTTPErrStatus(t, err, http.StatusServiceUnavailable)
}

func reviewDiffBatchRequest(t *testing.T, body dto.ReviewDiffBatchRequest) api.WorkspaceScopedJSON[dto.ReviewDiffBatchRequest] {
	t.Helper()
	gin.SetMode(gin.TestMode)
	ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
	ctx.Params = gin.Params{{Key: "reviewId", Value: "rcx_1"}}
	ctx.Request = httptest.NewRequest(http.MethodPost, "/", nil)
	return api.WorkspaceScopedJSON[dto.ReviewDiffBatchRequest]{
		WorkspaceScopedRequest: api.WorkspaceScopedRequest{
			AuthenticatedRequest: api.AuthenticatedRequest{
				Gin:  ctx,
				User: identity.AuthenticatedUser{User: model.User{ID: "usr_1"}},
			},
			Workspace: workspace.Summary{WorkspaceID: "wsp_1"},
		},
		Body: body,
	}
}

func requireHTTPErrStatus(t *testing.T, err error, status int) {
	t.Helper()
	var httpErr api.HTTPError
	require.ErrorAs(t, err, &httpErr)
	require.Equal(t, status, httpErr.Status)
}

// Silence the unused-import linter when errors is imported only for
// potential future test cases in this file.
var _ = errors.New
