package binding

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

	"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/store/identity"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
)

func TestCreateDerivesOwnerAndActorFromHumanRequest(t *testing.T) {
	fake := bindingServiceFixture()
	handler := newRouteHandlers(fake).collection
	request := humanBindingRequest(t, http.MethodPost, "/workflow/bindings", nil)
	request.Gin.Request.Header.Set(idempotencyHeader, "create-call-1")

	response, err := handler.Create(api.WorkspaceScopedJSON[dto.WorkflowBindingCreateRequest]{
		WorkspaceScopedRequest: request,
		Body: dto.WorkflowBindingCreateRequest{
			Key: "daily", DisplayName: "Daily", PlanID: "pln_1", PlanVersionID: "plv_1",
			Source: json.RawMessage(`{"type":"schedule","schedule":{"mode":"relative","relativeSeconds":60,"inputs":{}}}`), Activate: bindingBoolPointer(true),
		},
	})
	require.NoError(t, err)
	require.Equal(t, http.StatusCreated, response.Status)
	require.Equal(t, bindingETag("bnd_1", 4), response.Headers["ETag"][0])
	require.NotNil(t, fake.createInput)
	require.Equal(t, "wsp_1", fake.createInput.WorkspaceID)
	require.Equal(t, "pln_1", fake.createInput.PlanID)
	require.Equal(t, Owner{UserID: "usr_1", MemberID: "mem_1"}, fake.createInput.Owner)
	require.Equal(t, Actor{Type: ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"}, fake.createInput.Actor)
	require.Equal(t, "create-call-1", fake.createInput.IdempotencyKey)
	require.JSONEq(t, `{}`, string(fake.createInput.InputPolicy))
}

func bindingBoolPointer(value bool) *bool { return &value }

func TestCreateRevisionAndActivateUseBindingCAS(t *testing.T) {
	fake := bindingServiceFixture()
	handlers := newRouteHandlers(fake)

	createRequest := humanBindingRequest(t, http.MethodPost, "/workflow/bindings/bnd_1/revisions", map[string]string{"bindingId": "bnd_1"})
	createRequest.Gin.Request.Header.Set("If-Match", bindingETag("bnd_1", 3))
	createRequest.Gin.Request.Header.Set(idempotencyHeader, "revision-call-1")
	_, err := handlers.revisions.Create(api.WorkspaceScopedJSON[dto.WorkflowBindingRevisionCreateRequest]{
		WorkspaceScopedRequest: createRequest,
		Body:                   dto.WorkflowBindingRevisionCreateRequest{PlanVersionID: "plv_2", Source: json.RawMessage(`{"type":"schedule"}`)},
	})
	require.NoError(t, err)
	require.Equal(t, int64(3), fake.createRevisionInput.ExpectedRevision)
	require.Equal(t, Owner{UserID: "usr_1", MemberID: "mem_1"}, fake.createRevisionInput.Principal)
	require.Equal(t, "revision-call-1", fake.createRevisionInput.IdempotencyKey)

	activateRequest := humanBindingRequest(t, http.MethodPost, "/workflow/bindings/bnd_1/revisions/2/activate", map[string]string{"bindingId": "bnd_1", "revision": "2"})
	activateRequest.Gin.Request.Header.Set("If-Match", bindingETag("bnd_1", 4))
	activateRequest.Gin.Request.Header.Set(idempotencyHeader, "activate-call-1")
	response, err := handlers.revisions.Activate(activateRequest)
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, response.Status)
	require.Equal(t, int64(2), fake.activateInput.Revision)
	require.Equal(t, int64(4), fake.activateInput.ExpectedRevision)
	require.Equal(t, Owner{UserID: "usr_1", MemberID: "mem_1"}, fake.activateInput.Principal)
	require.Equal(t, "activate-call-1", fake.activateInput.IdempotencyKey)
}

func TestLifecycleRequiresIfMatchBeforeCallingService(t *testing.T) {
	fake := bindingServiceFixture()
	handler := newRouteHandlers(fake).detail
	request := humanBindingRequest(t, http.MethodPost, "/workflow/bindings/bnd_1/archive", map[string]string{"bindingId": "bnd_1"})
	_, err := handler.Archive(request)
	requireBindingHTTPError(t, err, http.StatusPreconditionRequired, "precondition_required")
	require.Nil(t, fake.lifecycleInput)
}

func TestHumanMutationsCarryCurrentOwnerPrincipal(t *testing.T) {
	fake := bindingServiceFixture()
	handlers := newRouteHandlers(fake)

	name := "Renamed"
	updateRequest := humanBindingRequest(t, http.MethodPatch, "/workflow/bindings/bnd_1", map[string]string{"bindingId": "bnd_1"})
	updateRequest.Gin.Request.Header.Set("If-Match", bindingETag("bnd_1", 4))
	updateRequest.Gin.Request.Header.Set(idempotencyHeader, "update-call-1")
	_, err := handlers.detail.Update(api.WorkspaceScopedJSON[dto.WorkflowBindingUpdateRequest]{
		WorkspaceScopedRequest: updateRequest,
		Body:                   dto.WorkflowBindingUpdateRequest{DisplayName: &name},
	})
	require.NoError(t, err)
	require.Equal(t, Owner{UserID: "usr_1", MemberID: "mem_1"}, fake.updateInput.Principal)
	require.Equal(t, Actor{Type: ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"}, fake.updateInput.Actor)
	require.Equal(t, "update-call-1", fake.updateInput.IdempotencyKey)

	lifecycleRequest := humanBindingRequest(t, http.MethodPost, "/workflow/bindings/bnd_1/archive", map[string]string{"bindingId": "bnd_1"})
	lifecycleRequest.Gin.Request.Header.Set("If-Match", bindingETag("bnd_1", 4))
	lifecycleRequest.Gin.Request.Header.Set(idempotencyHeader, "archive-call-1")
	_, err = handlers.detail.Archive(lifecycleRequest)
	require.NoError(t, err)
	require.Equal(t, Owner{UserID: "usr_1", MemberID: "mem_1"}, fake.lifecycleInput.Principal)
	require.Equal(t, Actor{Type: ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"}, fake.lifecycleInput.Actor)
	require.Equal(t, "archive-call-1", fake.lifecycleInput.IdempotencyKey)
}

func TestMetadataAndLifecycleMutationsRequireIdempotencyKey(t *testing.T) {
	fake := bindingServiceFixture()
	handlers := newRouteHandlers(fake)

	name := "Renamed"
	updateRequest := humanBindingRequest(t, http.MethodPatch, "/workflow/bindings/bnd_1", map[string]string{"bindingId": "bnd_1"})
	updateRequest.Gin.Request.Header.Set("If-Match", bindingETag("bnd_1", 4))
	_, err := handlers.detail.Update(api.WorkspaceScopedJSON[dto.WorkflowBindingUpdateRequest]{
		WorkspaceScopedRequest: updateRequest,
		Body:                   dto.WorkflowBindingUpdateRequest{DisplayName: &name},
	})
	requireBindingHTTPError(t, err, http.StatusBadRequest, "binding_idempotency_key_required")
	require.Nil(t, fake.updateInput)

	lifecycleRequest := humanBindingRequest(t, http.MethodPost, "/workflow/bindings/bnd_1/archive", map[string]string{"bindingId": "bnd_1"})
	lifecycleRequest.Gin.Request.Header.Set("If-Match", bindingETag("bnd_1", 4))
	_, err = handlers.detail.Archive(lifecycleRequest)
	requireBindingHTTPError(t, err, http.StatusBadRequest, "binding_idempotency_key_required")
	require.Nil(t, fake.lifecycleInput)
}

func TestCreateRequiresExplicitActivate(t *testing.T) {
	fake := bindingServiceFixture()
	handler := newRouteHandlers(fake).collection
	request := humanBindingRequest(t, http.MethodPost, "/workflow/bindings", nil)
	request.Gin.Request.Header.Set(idempotencyHeader, "create-call-1")
	_, err := handler.Create(api.WorkspaceScopedJSON[dto.WorkflowBindingCreateRequest]{
		WorkspaceScopedRequest: request,
		Body: dto.WorkflowBindingCreateRequest{
			Key: "daily", DisplayName: "Daily", PlanID: "pln_1", PlanVersionID: "plv_1", Source: json.RawMessage(`{"type":"schedule","schedule":{"mode":"relative","relativeSeconds":60,"inputs":{}}}`),
		},
	})
	requireBindingHTTPError(t, err, http.StatusBadRequest, "binding_invalid_request")
	require.Nil(t, fake.createInput)
}

func bindingServiceFixture() *fakeBindingService {
	now := time.Date(2026, 7, 21, 1, 2, 3, 0, time.UTC)
	actor := Actor{Type: ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"}
	return &fakeBindingService{
		binding: Binding{
			ID: "bnd_1", WorkspaceID: "wsp_1", PlanID: "pln_1", Key: "daily", DisplayName: "Daily",
			State: "enabled", Revision: 4, Owner: Owner{UserID: "usr_1", MemberID: "mem_1"},
			CreatedBy: actor, UpdatedBy: actor, CreatedAt: now, UpdatedAt: now,
		},
		revision: Revision{
			ID: "bnr_2", WorkspaceID: "wsp_1", BindingID: "bnd_1", PlanID: "pln_1", Revision: 2,
			PlanVersionID: "plv_2", Source: json.RawMessage(`{"type":"schedule","schedule":{"mode":"relative","relativeSeconds":60,"inputs":{}}}`), InputPolicy: json.RawMessage(`{}`),
			Digest: "digest-2", CreatedBy: actor, CreatedAt: now,
		},
	}
}

func humanBindingRequest(t *testing.T, method string, path string, params map[string]string) api.WorkspaceScopedRequest {
	t.Helper()
	ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
	ctx.Request = httptest.NewRequest(method, path, http.NoBody)
	for key, value := range params {
		ctx.Params = append(ctx.Params, gin.Param{Key: key, Value: value})
	}
	return api.WorkspaceScopedRequest{
		AuthenticatedRequest: api.AuthenticatedRequest{Gin: ctx, User: identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}}},
		Workspace:            workspace.Summary{WorkspaceID: "wsp_1", MemberID: "mem_1"},
	}
}
