package app

import (
	"context"
	"net/http"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/api/testkit/httputil"
	"github.com/tidbcloud/AgentTeam/internal/service/appruntime"
	storeapp "github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
)

type fakeAppRuntimeRouteStore struct {
	bearer    string
	heartbeat appruntime.RunnerHeartbeatParams
	claim     appruntime.RunnerClaimParams
	reply     storeapp.AppRuntimeReplyParams
	ack       storeapp.AppRuntimeAckParams
	fail      storeapp.AppRuntimeFailParams
}

func (fake *fakeAppRuntimeRouteStore) AuthenticateRunner(_ context.Context, bearer string) (appruntime.RunnerAuthentication, error) {
	fake.bearer = bearer
	return appruntime.RunnerAuthentication{WorkspaceID: "wsp_1", ServiceInstallationID: "appinst_1", ServiceRuntimeCredentialID: "svcrc_1"}, nil
}

func (fake *fakeAppRuntimeRouteStore) Heartbeat(_ context.Context, _ appruntime.RunnerAuthentication, params appruntime.RunnerHeartbeatParams) (appruntime.RunnerSessionView, error) {
	fake.heartbeat = params
	return appruntime.RunnerSessionView{ID: "svcrs_1", WorkspaceID: "wsp_1", ServiceInstallationID: "appinst_1", RunnerID: params.RunnerID, Status: "online"}, nil
}

func (fake *fakeAppRuntimeRouteStore) ClaimDeliveries(_ context.Context, _ appruntime.RunnerAuthentication, params appruntime.RunnerClaimParams) (appruntime.RunnerClaimResult, error) {
	fake.claim = params
	return appruntime.RunnerClaimResult{
		Session: appruntime.RunnerSessionView{ID: "svcrs_1", WorkspaceID: "wsp_1", ServiceInstallationID: "appinst_1", RunnerID: params.RunnerID, Status: "online"},
		Envelopes: []appruntime.RunnerDeliveryEnvelopeView{{
			EnvelopeID: "svcea_1", DeliveryToken: "delivery-token", EventType: "agentteam.message.mention", EventPayload: []byte(`{"message":{"id":"msg_1"}}`),
		}},
	}, nil
}

func (fake *fakeAppRuntimeRouteStore) MarkSessionOffline(_ context.Context, _ appruntime.RunnerAuthentication, sessionID string) (appruntime.RunnerSessionView, error) {
	return appruntime.RunnerSessionView{ID: sessionID, WorkspaceID: "wsp_1", ServiceInstallationID: "appinst_1", Status: "offline"}, nil
}

func (fake *fakeAppRuntimeRouteStore) AuthenticateAppRuntimeDelivery(_ context.Context, bearer, deliveryID, attemptID string) (storeapp.AppRuntimeDeliveryAuth, error) {
	fake.bearer = bearer
	return storeapp.AppRuntimeDeliveryAuth{WorkspaceID: "wsp_1", ServiceInstallationID: "appinst_1", DeliveryID: deliveryID, AttemptID: attemptID}, nil
}

func (fake *fakeAppRuntimeRouteStore) ReadAppRuntimeDeliveryContext(_ context.Context, _ storeapp.AppRuntimeDeliveryAuth, _ storeapp.AppRuntimeContextParams) (message.ScopeAPIMessagePage, error) {
	return message.ScopeAPIMessagePage{Messages: []message.AppMessage{{ID: "msg_1", Content: "hello app"}}}, nil
}

func (fake *fakeAppRuntimeRouteStore) ReplyAppRuntimeDelivery(_ context.Context, _ storeapp.AppRuntimeDeliveryAuth, params storeapp.AppRuntimeReplyParams) (storeapp.AppRuntimeReplyResult, error) {
	fake.reply = params
	return storeapp.AppRuntimeReplyResult{
		AppRuntimeDeliveryActionResult: storeapp.AppRuntimeDeliveryActionResult{
			Delivery: serviceactor.EventDelivery{ID: "svced_1", Status: "running"},
			Attempt:  serviceactor.DeliveryAttempt{ID: "svcea_1", Status: serviceactor.DeliveryAttemptStatusDelivered},
		},
		Effect: serviceactor.DeliveryEffect{ID: "svceff_1", EffectKind: "message", EffectRef: "msg_reply_1", MessageID: "msg_reply_1"}, MessageID: "msg_reply_1",
	}, nil
}

func (fake *fakeAppRuntimeRouteStore) AckAppRuntimeDelivery(_ context.Context, _ storeapp.AppRuntimeDeliveryAuth, params storeapp.AppRuntimeAckParams) (storeapp.AppRuntimeDeliveryActionResult, error) {
	fake.ack = params
	return storeapp.AppRuntimeDeliveryActionResult{Delivery: serviceactor.EventDelivery{ID: "svced_1", Status: "succeeded"}, Attempt: serviceactor.DeliveryAttempt{ID: "svcea_1", Status: serviceactor.DeliveryAttemptStatusAcked}}, nil
}

func (fake *fakeAppRuntimeRouteStore) FailAppRuntimeDelivery(_ context.Context, _ storeapp.AppRuntimeDeliveryAuth, params storeapp.AppRuntimeFailParams) (storeapp.AppRuntimeDeliveryActionResult, error) {
	fake.fail = params
	return storeapp.AppRuntimeDeliveryActionResult{Delivery: serviceactor.EventDelivery{ID: "svced_1", Status: "failed", ErrorCode: params.Code}, Attempt: serviceactor.DeliveryAttempt{ID: "svcea_1", Status: serviceactor.DeliveryAttemptStatusFailed}}, nil
}

func (*fakeAppRuntimeRouteStore) AuthenticateAppNotificationRuntime(context.Context, string, []string) (storeapp.AppNotificationRuntimePrincipal, error) {
	return storeapp.AppNotificationRuntimePrincipal{WorkspaceID: "wsp_1", ServiceInstallationID: "appinst_1"}, nil
}
func (*fakeAppRuntimeRouteStore) CreateAppNotificationForRuntime(context.Context, storeapp.AppNotificationRuntimePrincipal, storeapp.AppNotificationCommand) (storeapp.AppNotificationResult, error) {
	return storeapp.AppNotificationResult{NotificationID: "btn_1", Status: storeapp.AppNotificationStatusActive}, nil
}
func (*fakeAppRuntimeRouteStore) GetAppNotificationForRuntime(context.Context, storeapp.AppNotificationRuntimePrincipal, string) (storeapp.AppNotificationResult, error) {
	return storeapp.AppNotificationResult{NotificationID: "btn_1", Status: storeapp.AppNotificationStatusActive}, nil
}
func (*fakeAppRuntimeRouteStore) MutateAppNotificationForRuntime(context.Context, storeapp.AppNotificationRuntimePrincipal, storeapp.AppNotificationMutationCommand) (storeapp.AppNotificationResult, error) {
	return storeapp.AppNotificationResult{NotificationID: "btn_1", Status: storeapp.AppNotificationStatusActive}, nil
}
func (*fakeAppRuntimeRouteStore) MutateAppNotificationForDelivery(context.Context, storeapp.AppRuntimeDeliveryAuth, storeapp.AppNotificationMutationCommand) (storeapp.AppNotificationResult, error) {
	return storeapp.AppNotificationResult{NotificationID: "btn_1", Status: storeapp.AppNotificationStatusActive}, nil
}

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

	runtimeStore := &fakeAppRuntimeRouteStore{}
	router := newAppRouter(Dependencies{AppStore: newFakeStore(), AppRuntimeStore: runtimeStore, AppRunnerService: runtimeStore})

	resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/app-runtime/heartbeat", map[string]any{
		"runnerId": "runner-1", "runnerVersion": "1.2.3",
	})
	require.Equal(t, http.StatusOK, resp.Code)
	require.Equal(t, "token", runtimeStore.bearer)
	require.Equal(t, "runner-1", runtimeStore.heartbeat.RunnerID)

	resp = httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/app-runtime/events:claim", map[string]any{
		"runnerId": "runner-1", "limit": 3, "leaseSeconds": 45,
	})
	require.Equal(t, http.StatusOK, resp.Code)
	require.Equal(t, 3, runtimeStore.claim.Limit)
	require.Equal(t, 45*time.Second, runtimeStore.claim.LeaseDuration)
	require.Contains(t, resp.Body.String(), "delivery-token")

	resp = httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_2/app-runtime/heartbeat", map[string]any{
		"runnerId": "wrong-workspace-runner",
	})
	require.Equal(t, http.StatusBadRequest, resp.Code)
	require.Equal(t, "runner-1", runtimeStore.heartbeat.RunnerID)

	resp = httputil.DoJSON(t, router, http.MethodPost, "/internal/app/runtime/heartbeat", map[string]any{"runnerId": "legacy-runner"})
	require.Equal(t, http.StatusNotFound, resp.Code)
	require.Equal(t, "runner-1", runtimeStore.heartbeat.RunnerID)

	resp = httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/app-runtime/stream?runnerId=stream-runner&once=true&limit=1&leaseSeconds=45", nil)
	require.Equal(t, http.StatusOK, resp.Code)
	require.Equal(t, "application/x-ndjson", resp.Header().Get("Content-Type"))
	require.Equal(t, "stream-runner", runtimeStore.claim.RunnerID)
	require.Contains(t, resp.Body.String(), `"type":"session"`)
	require.Contains(t, resp.Body.String(), `"type":"delivery"`)
}

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

	runtimeStore := &fakeAppRuntimeRouteStore{}
	router := newAppRouter(Dependencies{AppStore: newFakeStore(), AppRuntimeStore: runtimeStore})

	resp := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/app-runtime/deliveries/svced_1/context?attemptId=svcea_1&limit=20", nil)
	require.Equal(t, http.StatusOK, resp.Code)
	require.Contains(t, resp.Body.String(), "hello app")

	resp = httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/app-runtime/deliveries/svced_1/reply", map[string]any{
		"attemptId": "svcea_1", "content": "done", "idempotencyKey": "reply-1",
	})
	require.Equal(t, http.StatusOK, resp.Code)
	require.Equal(t, "done", runtimeStore.reply.Content)
	require.Equal(t, "reply-1", runtimeStore.reply.IdempotencyKey)
	require.Contains(t, resp.Body.String(), "msg_reply_1")

	resp = httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/app-runtime/deliveries/svced_1/ack", map[string]any{
		"attemptId": "svcea_1", "result": map[string]any{"status": "ok"},
	})
	require.Equal(t, http.StatusOK, resp.Code)
	require.Equal(t, "svcea_1", runtimeStore.ack.AttemptID)
	require.Contains(t, resp.Body.String(), "acked")

	resp = httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/app-runtime/deliveries/svced_1/fail", map[string]any{
		"attemptId": "svcea_1", "code": "upstream_failed", "message": "retry later",
	})
	require.Equal(t, http.StatusOK, resp.Code)
	require.Equal(t, "svcea_1", runtimeStore.fail.AttemptID)
	require.Equal(t, "upstream_failed", runtimeStore.fail.Code)
	require.Contains(t, resp.Body.String(), "failed")
}

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

	router := newAppRouter(Dependencies{AppStore: newFakeStore()})
	for _, request := range []struct {
		method string
		path   string
		body   map[string]any
	}{
		{method: http.MethodGet, path: "/api/v1/workspaces/wsp_1/app-installations/appinst_1/app-runtime-diagnostics"},
		{method: http.MethodPost, path: "/api/v1/workspaces/wsp_1/app-installations/appinst_1/app-runtime-delivery-cancellations", body: map[string]any{"reason": "maintenance"}},
		{method: http.MethodPost, path: "/api/v1/workspaces/wsp_1/runner-credentials/svcrc_1:rotate"},
		{method: http.MethodPost, path: "/api/v1/workspaces/wsp_1/runner-credentials/svcrc_1:disable"},
	} {
		response := httputil.DoJSON(t, router, request.method, request.path, request.body)
		require.Equal(t, http.StatusNotFound, response.Code, "%s %s", request.method, request.path)
	}

	for _, request := range []struct {
		method string
		path   string
		body   map[string]any
	}{
		{method: http.MethodGet, path: "/api/v1/workspaces/wsp_1/app-installations/appinst_1/runner-credentials"},
		{method: http.MethodPost, path: "/api/v1/workspaces/wsp_1/app-installations/appinst_1/runner-credentials", body: map[string]any{}},
		{method: http.MethodPost, path: "/api/v1/workspaces/wsp_1/app-installations/appinst_1/callback-secret:rotate"},
		{method: http.MethodDelete, path: "/api/v1/workspaces/wsp_1/runner-credentials/svcrc_1"},
	} {
		response := httputil.DoJSON(t, router, request.method, request.path, request.body)
		require.Equal(t, http.StatusOK, response.Code, "%s %s", request.method, request.path)
	}
}
