package appruntime

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

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/core/webhook"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
)

type fakeCallbackStore struct {
	config                   serviceactor.RuntimeCallbackConfig
	envelopes                []serviceactor.DeliveryEnvelope
	claimParams              serviceactor.DeliveryDispatchClaim
	capabilityToken          string
	capabilityInstallationID string
	capabilityRequestID      string
	capabilityScopes         []string
	capabilityTTL            time.Duration
	capabilityError          error
	deliveredMark            *serviceactor.DeliveryAttemptDeliveredMark
	retrySchedule            *serviceactor.DeliveryAttemptRetrySchedule
	markError                error
	loadedAttempt            serviceactor.DeliveryAttempt
}

func (store *fakeCallbackStore) LoadRuntimeCallbackConfig(context.Context, string, string) (serviceactor.RuntimeCallbackConfig, error) {
	return store.config, nil
}

func (store *fakeCallbackStore) ClaimDeliveryEnvelopes(_ context.Context, params serviceactor.DeliveryDispatchClaim) ([]serviceactor.DeliveryEnvelope, error) {
	store.claimParams = params
	return append([]serviceactor.DeliveryEnvelope(nil), store.envelopes...), nil
}

func (store *fakeCallbackStore) IssueAppCapabilityToken(_ context.Context, installationID string, requestID string, scopes []string, ttl time.Duration) (string, error) {
	store.capabilityInstallationID = installationID
	store.capabilityRequestID = requestID
	store.capabilityScopes = append([]string(nil), scopes...)
	store.capabilityTTL = ttl
	if store.capabilityError != nil {
		return "", store.capabilityError
	}
	if store.capabilityToken == "" {
		store.capabilityToken = "capability-token"
	}
	return store.capabilityToken, nil
}

func (store *fakeCallbackStore) MarkDeliveryAttemptDelivered(_ context.Context, params serviceactor.DeliveryAttemptDeliveredMark) (serviceactor.DeliveryAttempt, error) {
	store.deliveredMark = &params
	if store.markError != nil {
		return serviceactor.DeliveryAttempt{}, store.markError
	}
	return serviceactor.DeliveryAttempt{ID: params.AttemptID, WorkspaceID: params.WorkspaceID, Status: serviceactor.DeliveryAttemptStatusDelivered}, nil
}

func (store *fakeCallbackStore) LoadDeliveryAttempt(_ context.Context, workspaceID string, attemptID string) (serviceactor.DeliveryAttempt, error) {
	if store.loadedAttempt.ID != "" {
		return store.loadedAttempt, nil
	}
	return serviceactor.DeliveryAttempt{ID: attemptID, WorkspaceID: workspaceID, Status: serviceactor.DeliveryAttemptStatusLeased}, nil
}

func (store *fakeCallbackStore) ScheduleDeliveryAttemptRetry(_ context.Context, params serviceactor.DeliveryAttemptRetrySchedule) (serviceactor.DeliveryAttemptRetryResult, error) {
	store.retrySchedule = &params
	return serviceactor.DeliveryAttemptRetryResult{Attempt: serviceactor.DeliveryAttempt{ID: params.AttemptID, WorkspaceID: params.WorkspaceID, Status: serviceactor.DeliveryAttemptStatusFailed}}, nil
}

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

	now := time.Date(2026, 6, 1, 8, 0, 0, 0, time.UTC)
	secret := "callback-secret"
	var received map[string]any
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		require.Equal(t, "/events", request.URL.Path)
		body := readRequestBodyForTest(t, request)
		require.Equal(t, webhook.RemoteCallbackSignature(secret, body), request.Header.Get("AgentTeam-Signature"))
		require.Equal(t, "Bearer delivery-token", request.Header.Get("Authorization"))
		require.Equal(t, "capability-token", request.Header.Get(callbackCapabilityTokenHeader))
		require.Equal(t, "del_1", request.Header.Get("AgentTeam-Delivery-ID"))
		require.Equal(t, "att_1", request.Header.Get("AgentTeam-Attempt-ID"))
		require.NoError(t, json.Unmarshal(body, &received))
		writer.WriteHeader(http.StatusAccepted)
		_, _ = writer.Write([]byte("accepted"))
	}))
	defer server.Close()

	store := &fakeCallbackStore{
		config: serviceactor.RuntimeCallbackConfig{WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1", CallbackURL: server.URL, CallbackSigningSecret: secret},
		envelopes: []serviceactor.DeliveryEnvelope{{
			EnvelopeID:    "att_1",
			Delivery:      serviceactor.EventDelivery{ID: "del_1", WorkspaceID: "wsp_1"},
			Attempt:       serviceactor.DeliveryAttempt{ID: "att_1", WorkspaceID: "wsp_1"},
			DeliveryToken: "delivery-token",
			EventType:     "agentteam.message.mention",
			EventAction:   "mention",
			EventPayload:  []byte(`{"message":{"id":"msg_1"}}`),
		}},
	}
	service := NewCallbackService(CallbackServiceOptions{Store: store, HTTPClient: server.Client(), Now: func() time.Time { return now }, AllowInsecureCallbackURL: true})

	result, err := service.DispatchCallbackDeliveries(context.Background(), CallbackDispatchParams{WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1", SubscriptionKind: serviceactor.SubscriptionKindAppMention, Limit: 5})
	require.NoError(t, err)
	require.Equal(t, 1, result.Attempted)
	require.Equal(t, 1, result.Delivered)
	require.Equal(t, 0, result.Retried)
	require.Equal(t, serviceactor.RuntimeTransportKindCallback, store.claimParams.TransportKind)
	require.Equal(t, serviceactor.AppRuntimeDeliveryPolicyExternal, store.claimParams.DeliveryPolicy)
	require.Equal(t, serviceactor.AppRuntimeConnectionRefExternal, store.claimParams.ServiceConnectionRef)
	require.Equal(t, "callback:svcinst_1", store.claimParams.LeaseOwner)
	require.Equal(t, server.URL+"/events", store.claimParams.CallbackURLSnapshot)
	require.NotNil(t, store.deliveredMark)
	require.Equal(t, "att_1", store.deliveredMark.AttemptID)
	require.Equal(t, "svcinst_1", store.capabilityInstallationID)
	require.Equal(t, "app-callback:del_1:att_1", store.capabilityRequestID)
	require.Empty(t, store.capabilityScopes)
	require.Equal(t, time.Minute, store.capabilityTTL)
	require.NotNil(t, store.deliveredMark.ResponseStatusCode)
	require.Equal(t, http.StatusAccepted, *store.deliveredMark.ResponseStatusCode)
	require.Equal(t, "accepted", store.deliveredMark.ResponseExcerpt)
	require.Nil(t, store.retrySchedule)
	require.Equal(t, "att_1", received["envelope_id"])
	require.Equal(t, "del_1", received["delivery_id"])
	require.Equal(t, "att_1", received["attempt_id"])
	require.Equal(t, "agentteam.message.mention", received["event_type"])
	require.Equal(t, "delivery-token", received["delivery_token"])
	require.Equal(t, "capability-token", received["capability_token"])
}

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

	now := time.Date(2026, 6, 1, 8, 10, 0, 0, time.UTC)
	requests := 0
	server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
		requests++
	}))
	defer server.Close()

	store := &fakeCallbackStore{
		config: serviceactor.RuntimeCallbackConfig{WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1", CallbackURL: server.URL, CallbackSigningSecret: "secret"},
		envelopes: []serviceactor.DeliveryEnvelope{{
			EnvelopeID: "att_1",
			Delivery: serviceactor.EventDelivery{
				ID: "del_1", WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1",
			},
			Attempt:       serviceactor.DeliveryAttempt{ID: "att_1", WorkspaceID: "wsp_1"},
			DeliveryToken: "delivery-token",
			EventType:     "agentteam.message.mention",
			EventPayload:  []byte(`{}`),
		}},
		capabilityError: errors.New("capability unavailable"),
	}
	service := NewCallbackService(CallbackServiceOptions{Store: store, HTTPClient: server.Client(), Now: func() time.Time { return now }, AllowInsecureCallbackURL: true})

	result, err := service.DispatchCallbackDeliveries(context.Background(), CallbackDispatchParams{WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1"})
	require.NoError(t, err)
	require.Equal(t, 1, result.Retried)
	require.Zero(t, requests)
	require.Nil(t, store.deliveredMark)
	require.NotNil(t, store.retrySchedule)
	require.Equal(t, CallbackErrorRequestFailed, store.retrySchedule.ErrorCode)
	require.Contains(t, store.retrySchedule.ErrorDetail, "capability unavailable")
}

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

	now := time.Date(2026, 6, 1, 8, 30, 0, 0, time.UTC)
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		writer.WriteHeader(http.StatusServiceUnavailable)
		_, _ = writer.Write([]byte("try later"))
	}))
	defer server.Close()

	store := &fakeCallbackStore{
		config: serviceactor.RuntimeCallbackConfig{WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1", CallbackURL: server.URL, CallbackSigningSecret: "secret"},
		envelopes: []serviceactor.DeliveryEnvelope{{
			EnvelopeID:    "att_1",
			Delivery:      serviceactor.EventDelivery{ID: "del_1", WorkspaceID: "wsp_1"},
			Attempt:       serviceactor.DeliveryAttempt{ID: "att_1", WorkspaceID: "wsp_1"},
			DeliveryToken: "delivery-token",
			EventType:     "agentteam.message.mention",
			EventPayload:  []byte(`{}`),
		}},
	}
	service := NewCallbackService(CallbackServiceOptions{Store: store, HTTPClient: server.Client(), Now: func() time.Time { return now }, BaseBackoff: time.Minute, MaxBackoff: 5 * time.Minute, AllowInsecureCallbackURL: true})

	result, err := service.DispatchCallbackDeliveries(context.Background(), CallbackDispatchParams{WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1"})
	require.NoError(t, err)
	require.Equal(t, 1, result.Attempted)
	require.Equal(t, 0, result.Delivered)
	require.Equal(t, 1, result.Retried)
	require.Nil(t, store.deliveredMark)
	require.NotNil(t, store.retrySchedule)
	require.Equal(t, CallbackErrorHTTPStatus, store.retrySchedule.ErrorCode)
	require.Contains(t, store.retrySchedule.ErrorDetail, "HTTP 503")
	require.NotNil(t, store.retrySchedule.ResponseStatus)
	require.Equal(t, http.StatusServiceUnavailable, *store.retrySchedule.ResponseStatus)
	require.Equal(t, "try later", store.retrySchedule.ResponseBody)
	require.Equal(t, time.Minute, store.retrySchedule.BaseBackoff)
	require.Equal(t, 5*time.Minute, store.retrySchedule.MaxBackoff)
}

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

	now := time.Date(2026, 6, 1, 8, 20, 0, 0, time.UTC)
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		writer.WriteHeader(http.StatusAccepted)
		_, _ = writer.Write([]byte("accepted"))
	}))
	defer server.Close()

	store := &fakeCallbackStore{
		config: serviceactor.RuntimeCallbackConfig{WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1", CallbackURL: server.URL, CallbackSigningSecret: "secret"},
		envelopes: []serviceactor.DeliveryEnvelope{{
			EnvelopeID:    "att_1",
			Delivery:      serviceactor.EventDelivery{ID: "del_1", WorkspaceID: "wsp_1"},
			Attempt:       serviceactor.DeliveryAttempt{ID: "att_1", WorkspaceID: "wsp_1"},
			DeliveryToken: "delivery-token",
			EventType:     "agentteam.message.mention",
			EventPayload:  []byte(`{}`),
		}},
		markError:     errors.New("service event delivery attempt is not leased"),
		loadedAttempt: serviceactor.DeliveryAttempt{ID: "att_1", WorkspaceID: "wsp_1", Status: serviceactor.DeliveryAttemptStatusAcked},
	}
	service := NewCallbackService(CallbackServiceOptions{Store: store, HTTPClient: server.Client(), Now: func() time.Time { return now }, AllowInsecureCallbackURL: true})

	result, err := service.DispatchCallbackDeliveries(context.Background(), CallbackDispatchParams{WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1"})
	require.NoError(t, err)
	require.Equal(t, 1, result.Attempted)
	require.Equal(t, 1, result.Delivered)
	require.Equal(t, 0, result.Retried)
	require.NotNil(t, store.deliveredMark)
	require.Nil(t, store.retrySchedule)
}

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

	now := time.Date(2026, 6, 1, 9, 0, 0, 0, time.UTC)
	store := &fakeCallbackStore{
		config: serviceactor.RuntimeCallbackConfig{WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1", CallbackURL: "https://app.example.com", CallbackSigningSecret: "secret"},
	}
	service := NewCallbackService(CallbackServiceOptions{
		Store: store,
		Now:   func() time.Time { return now },
		ResolveCallbackHostIPs: func(context.Context, string) ([]net.IP, error) {
			return []net.IP{net.ParseIP("93.184.216.34")}, nil
		},
	})

	result, err := service.DispatchCallbackDeliveries(context.Background(), CallbackDispatchParams{WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1"})
	require.NoError(t, err)
	require.Equal(t, 0, result.Attempted)
	require.Equal(t, serviceactor.SubscriptionKindAppMention, store.claimParams.SubscriptionKind)
}

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

	store := &fakeCallbackStore{config: serviceactor.RuntimeCallbackConfig{WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1", CallbackURL: "https://app.example.com", CallbackSigningSecret: "secret"}}
	service := NewCallbackService(CallbackServiceOptions{
		Store: store,
		ResolveCallbackHostIPs: func(context.Context, string) ([]net.IP, error) {
			return []net.IP{net.ParseIP("93.184.216.34")}, nil
		},
	})

	_, err := service.DispatchCallbackDeliveries(context.Background(), CallbackDispatchParams{WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1", SubscriptionKind: serviceactor.SubscriptionKindPullRequest})
	require.Error(t, err)
	require.Contains(t, err.Error(), "not supported")
}

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

	store := &fakeCallbackStore{config: serviceactor.RuntimeCallbackConfig{WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1", CallbackURL: "https://app.example.com", CallbackSigningSecret: "secret"}}
	service := NewCallbackService(CallbackServiceOptions{
		Store: store,
		ResolveCallbackHostIPs: func(context.Context, string) ([]net.IP, error) {
			return []net.IP{net.ParseIP("93.184.216.34")}, nil
		},
	})

	result, err := service.DispatchCallbackDeliveries(context.Background(), CallbackDispatchParams{
		WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1",
		SubscriptionKind: serviceactor.SubscriptionKindAppNotificationAction,
	})
	require.NoError(t, err)
	require.Zero(t, result.Attempted)
	require.Equal(t, serviceactor.SubscriptionKindAppNotificationAction, store.claimParams.SubscriptionKind)
}

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

	ctx := context.Background()
	resolver := func(_ context.Context, host string) ([]net.IP, error) {
		switch host {
		case "app.example.com":
			return []net.IP{net.ParseIP("93.184.216.34")}, nil
		case "private.example.com":
			return []net.IP{net.ParseIP("10.0.0.5")}, nil
		default:
			return []net.IP{net.ParseIP("93.184.216.34")}, nil
		}
	}

	safe, err := callbackEventsURL(ctx, "https://app.example.com/callback", CallbackURLPolicy{ResolveHostIPs: resolver})
	require.NoError(t, err)
	require.Equal(t, "https://app.example.com/callback/events", safe)

	for _, input := range []string{
		"http://app.example.com/callback",
		"https://127.0.0.1/callback",
		"https://localhost/callback",
		"https://169.254.169.254/callback",
		"https://10.0.0.5/callback",
		"https://100.64.0.1/callback",
		"https://198.18.0.1/callback",
		"https://203.0.113.10/callback",
		"https://private.example.com/callback",
		"https://user:pass@app.example.com/callback",
		"https://app.example.com:8443/callback",
		"https://app.example.com/callback?token=secret",
	} {
		_, err := callbackEventsURL(ctx, input, CallbackURLPolicy{ResolveHostIPs: resolver})
		require.Error(t, err, input)
	}
}

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

	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		http.Redirect(writer, request, "http://169.254.169.254/latest/meta-data", http.StatusTemporaryRedirect)
	}))
	defer server.Close()

	store := &fakeCallbackStore{
		config: serviceactor.RuntimeCallbackConfig{WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1", CallbackURL: server.URL, CallbackSigningSecret: "secret"},
		envelopes: []serviceactor.DeliveryEnvelope{{
			EnvelopeID:    "att_1",
			Delivery:      serviceactor.EventDelivery{ID: "del_1", WorkspaceID: "wsp_1"},
			Attempt:       serviceactor.DeliveryAttempt{ID: "att_1", WorkspaceID: "wsp_1"},
			DeliveryToken: "delivery-token",
			EventType:     "agentteam.message.mention",
			EventPayload:  []byte(`{}`),
		}},
	}
	service := NewCallbackService(CallbackServiceOptions{Store: store, AllowInsecureCallbackURL: true})

	result, err := service.DispatchCallbackDeliveries(context.Background(), CallbackDispatchParams{WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1"})
	require.NoError(t, err)
	require.Equal(t, 1, result.Retried)
	require.Nil(t, store.deliveredMark)
	require.NotNil(t, store.retrySchedule)
	require.Equal(t, CallbackErrorRequestFailed, store.retrySchedule.ErrorCode)
	require.Contains(t, store.retrySchedule.ErrorDetail, "redirect")
}

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

	resolveCalls := 0
	resolver := func(context.Context, string) ([]net.IP, error) {
		resolveCalls++
		if resolveCalls == 1 {
			return []net.IP{net.ParseIP("93.184.216.34")}, nil
		}
		return []net.IP{net.ParseIP("10.0.0.5")}, nil
	}
	store := &fakeCallbackStore{
		config: serviceactor.RuntimeCallbackConfig{WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1", CallbackURL: "https://rebind.example.com/callback", CallbackSigningSecret: "secret"},
		envelopes: []serviceactor.DeliveryEnvelope{{
			EnvelopeID:    "att_1",
			Delivery:      serviceactor.EventDelivery{ID: "del_1", WorkspaceID: "wsp_1"},
			Attempt:       serviceactor.DeliveryAttempt{ID: "att_1", WorkspaceID: "wsp_1"},
			DeliveryToken: "delivery-token",
			EventType:     "agentteam.message.mention",
			EventPayload:  []byte(`{}`),
		}},
	}
	service := NewCallbackService(CallbackServiceOptions{Store: store, ResolveCallbackHostIPs: resolver})

	result, err := service.DispatchCallbackDeliveries(context.Background(), CallbackDispatchParams{WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1"})
	require.NoError(t, err)
	require.GreaterOrEqual(t, resolveCalls, 2)
	require.Equal(t, 1, result.Retried)
	require.Nil(t, store.deliveredMark)
	require.NotNil(t, store.retrySchedule)
	require.Equal(t, CallbackErrorRequestFailed, store.retrySchedule.ErrorCode)
	require.Contains(t, store.retrySchedule.ErrorDetail, "public IP")
}

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

	store := &fakeCallbackStore{config: serviceactor.RuntimeCallbackConfig{WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1"}}
	service := NewCallbackService(CallbackServiceOptions{Store: store})

	_, err := service.DispatchCallbackDeliveries(context.Background(), CallbackDispatchParams{WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1"})
	require.Error(t, err)
	require.Contains(t, err.Error(), "callback URL")
}

func readRequestBodyForTest(t *testing.T, request *http.Request) []byte {
	t.Helper()
	defer func() { _ = request.Body.Close() }()
	body, err := io.ReadAll(request.Body)
	require.NoError(t, err)
	return body
}
