package appruntime

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

type fakeRunnerStore struct {
	credential  serviceactor.RuntimeCredential
	authToken   string
	authErr     error
	upsert      serviceactor.RuntimeSessionUpsert
	session     serviceactor.RuntimeSession
	loaded      serviceactor.RuntimeSession
	claim       serviceactor.DeliveryDispatchClaim
	claimCount  int
	envelopes   []serviceactor.DeliveryEnvelope
	statusID    string
	statusValue string
}

type fakeRunnerAuthority struct {
	credential       serviceactor.RuntimeCredential
	kinds            []string
	validateErr      error
	loadErr          error
	loadedWorkspace  string
	loadedCredential string
}

func (authority *fakeRunnerAuthority) ValidateAppRunnerCredential(_ context.Context, credential serviceactor.RuntimeCredential) (serviceactor.RuntimeCredential, error) {
	if authority.validateErr != nil {
		return serviceactor.RuntimeCredential{}, authority.validateErr
	}
	return credential, nil
}

func (authority *fakeRunnerAuthority) LoadCurrentAppRunnerCredential(_ context.Context, workspaceID, credentialID string) (serviceactor.RuntimeCredential, error) {
	authority.loadedWorkspace = workspaceID
	authority.loadedCredential = credentialID
	if authority.loadErr != nil {
		return serviceactor.RuntimeCredential{}, authority.loadErr
	}
	return authority.credential, nil
}

func (authority *fakeRunnerAuthority) AppRunnerClaimSubscriptionKinds(_ context.Context, _, _ string) ([]string, error) {
	if authority.kinds == nil {
		return serviceactor.AppRuntimeSubscriptionKinds(), nil
	}
	return append([]string(nil), authority.kinds...), nil
}

func (store *fakeRunnerStore) AuthenticateRuntimeCredential(_ context.Context, token string) (serviceactor.RuntimeCredential, error) {
	store.authToken = token
	if store.authErr != nil {
		return serviceactor.RuntimeCredential{}, store.authErr
	}
	return store.credential, nil
}

func (store *fakeRunnerStore) UpsertRuntimeSession(_ context.Context, params serviceactor.RuntimeSessionUpsert) (serviceactor.RuntimeSession, error) {
	store.upsert = params
	if store.session.ID != "" {
		return store.session, nil
	}
	return serviceactor.RuntimeSession{
		ID:                         "svcrs_1",
		WorkspaceID:                params.WorkspaceID,
		ServiceDefinitionID:        params.ServiceDefinitionID,
		ServiceInstallationID:      params.ServiceInstallationID,
		ServiceMemberID:            params.ServiceMemberID,
		ServiceRuntimeCredentialID: params.ServiceRuntimeCredentialID,
		RunnerID:                   params.RunnerID,
		Status:                     params.Status,
		LastHeartbeatAt:            params.LastHeartbeatAt,
	}, nil
}

func (store *fakeRunnerStore) LoadRuntimeSession(_ context.Context, workspaceID string, sessionID string) (serviceactor.RuntimeSession, error) {
	if store.loaded.ID != "" {
		return store.loaded, nil
	}
	return serviceactor.RuntimeSession{
		ID:                         sessionID,
		WorkspaceID:                workspaceID,
		ServiceInstallationID:      store.session.ServiceInstallationID,
		ServiceRuntimeCredentialID: store.session.ServiceRuntimeCredentialID,
		RunnerID:                   store.session.RunnerID,
		Status:                     store.session.Status,
		LastHeartbeatAt:            store.session.LastHeartbeatAt,
		LastClaimAt:                store.session.LastClaimAt,
	}, nil
}

func (store *fakeRunnerStore) SetRuntimeSessionStatus(_ context.Context, workspaceID string, sessionID string, status string, now time.Time) (serviceactor.RuntimeSession, error) {
	store.statusID = sessionID
	store.statusValue = status
	return serviceactor.RuntimeSession{ID: sessionID, WorkspaceID: workspaceID, Status: status, UpdatedAt: now}, nil
}

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

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

	store := &fakeRunnerStore{
		credential: runnerCredential(),
		session:    serviceactor.RuntimeSession{ID: "svcrs_runner", WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1", ServiceRuntimeCredentialID: "svcrc_1", Status: serviceactor.RuntimeSessionStatusOnline},
	}
	authority := &fakeRunnerAuthority{credential: store.credential, kinds: []string{serviceactor.SubscriptionKindAppNotificationAction, serviceactor.SubscriptionKindAppNotificationAction}}
	service := NewRunnerService(RunnerServiceOptions{Store: store, Authority: authority})
	auth, err := service.AuthenticateRunner(context.Background(), "runner-token")
	require.NoError(t, err)

	_, err = service.ClaimDeliveries(context.Background(), auth, RunnerClaimParams{RunnerID: "runner-action"})
	require.NoError(t, err)
	require.Equal(t, []string{serviceactor.SubscriptionKindAppNotificationAction}, store.claim.SubscriptionKinds)
	require.Equal(t, 1, store.claimCount)

	authority.kinds = []string{}
	_, err = service.ClaimDeliveries(context.Background(), auth, RunnerClaimParams{RunnerID: "runner-empty"})
	require.NoError(t, err)
	require.Equal(t, 1, store.claimCount, "explicitly empty kind allowlist must not query deliveries")

	authority.kinds = []string{serviceactor.SubscriptionKindPullRequest}
	_, err = service.ClaimDeliveries(context.Background(), auth, RunnerClaimParams{RunnerID: "runner-invalid"})
	require.Error(t, err)
	require.Equal(t, 1, store.claimCount)
}

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

	now := time.Date(2026, 6, 1, 10, 0, 0, 0, time.UTC)
	store := &fakeRunnerStore{credential: runnerCredential()}
	service := NewRunnerService(RunnerServiceOptions{Store: store, Authority: &fakeRunnerAuthority{credential: store.credential}, Now: func() time.Time { return now }})

	auth, err := service.AuthenticateRunner(context.Background(), " runner-token ")
	require.NoError(t, err)
	require.Equal(t, "runner-token", store.authToken)

	session, err := service.Heartbeat(context.Background(), auth, RunnerHeartbeatParams{
		RunnerID:      "runner-east-1",
		RunnerVersion: "1.2.3",
		Capabilities:  map[string]any{"maxConcurrency": float64(2)},
	})
	require.NoError(t, err)
	require.Equal(t, "svcrs_1", session.ID)
	require.Equal(t, serviceactor.RuntimeSessionStatusOnline, store.upsert.Status)
	require.Equal(t, "runner-east-1", store.upsert.RunnerID)
	require.Equal(t, "1.2.3", store.upsert.RunnerVersion)
	require.Equal(t, []byte(`{"maxConcurrency":2}`), store.upsert.CapabilitiesJSON)
	require.JSONEq(t, `{"transport":"runner_stream"}`, string(store.upsert.Metadata))
	require.NotNil(t, store.upsert.LastHeartbeatAt)
	require.Equal(t, now, *store.upsert.LastHeartbeatAt)
}

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

	store := &fakeRunnerStore{credential: runnerCredential()}
	authority := &fakeRunnerAuthority{credential: store.credential}
	service := NewRunnerService(RunnerServiceOptions{Store: store, Authority: authority})
	auth, err := service.AuthenticateRunner(context.Background(), "runner-token")
	require.NoError(t, err)

	authority.loadErr = storeerr.ErrUnauthorized
	_, err = service.Heartbeat(context.Background(), auth, RunnerHeartbeatParams{RunnerID: "runner-east-1"})
	require.ErrorIs(t, err, storeerr.ErrUnauthorized)
	require.Equal(t, "wsp_1", authority.loadedWorkspace)
	require.Equal(t, "svcrc_1", authority.loadedCredential)
	require.Empty(t, store.upsert.RunnerID, "revoked credentials must not update a session")
}

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

	for name, authErr := range map[string]error{
		"not found":   serviceactor.ErrNotFound,
		"bad request": serviceactor.ErrBadRequest,
	} {
		t.Run(name, func(t *testing.T) {
			t.Parallel()
			service := NewRunnerService(RunnerServiceOptions{
				Store:     &fakeRunnerStore{authErr: authErr},
				Authority: &fakeRunnerAuthority{},
			})
			_, err := service.AuthenticateRunner(context.Background(), "runner-token")
			require.ErrorIs(t, err, storeerr.ErrUnauthorized)
		})
	}
}

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

	now := time.Date(2026, 6, 1, 10, 30, 0, 0, time.UTC)
	claimedAt := now.Add(time.Second)
	store := &fakeRunnerStore{
		credential: runnerCredential(),
		session:    serviceactor.RuntimeSession{ID: "svcrs_runner", WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1", ServiceRuntimeCredentialID: "svcrc_1", RunnerID: "runner-east-1", Status: serviceactor.RuntimeSessionStatusOnline},
		loaded:     serviceactor.RuntimeSession{ID: "svcrs_runner", WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1", ServiceRuntimeCredentialID: "svcrc_1", RunnerID: "runner-east-1", Status: serviceactor.RuntimeSessionStatusOnline, LastClaimAt: &claimedAt},
		envelopes: []serviceactor.DeliveryEnvelope{{
			EnvelopeID:    "svcea_1",
			Delivery:      serviceactor.EventDelivery{ID: "svced_1", WorkspaceID: "wsp_1"},
			Attempt:       serviceactor.DeliveryAttempt{ID: "svcea_1", WorkspaceID: "wsp_1"},
			DeliveryToken: "delivery-token",
			EventType:     "agentteam.message.mention",
		}},
	}
	service := NewRunnerService(RunnerServiceOptions{Store: store, Authority: &fakeRunnerAuthority{credential: store.credential}, Now: func() time.Time { return now }})
	auth, err := service.AuthenticateRunner(context.Background(), "runner-token")
	require.NoError(t, err)

	result, err := service.ClaimDeliveries(context.Background(), auth, RunnerClaimParams{RunnerID: "runner-east-1", Limit: 25, LeaseDuration: time.Minute})
	require.NoError(t, err)
	require.Equal(t, "svcrs_runner", result.Session.ID)
	require.NotNil(t, result.Session.LastClaimAt)
	require.Len(t, result.Envelopes, 1)
	require.Empty(t, store.claim.SubscriptionKind)
	require.Equal(t, serviceactor.AppRuntimeSubscriptionKinds(), store.claim.SubscriptionKinds)
	require.Equal(t, serviceactor.RuntimeTransportKindRunnerStream, store.claim.TransportKind)
	require.Equal(t, serviceactor.AppRuntimeDeliveryPolicyExternal, store.claim.DeliveryPolicy)
	require.Equal(t, serviceactor.AppRuntimeConnectionRefExternal, store.claim.ServiceConnectionRef)
	require.Equal(t, "svcrc_1", store.claim.ServiceRuntimeCredentialID)
	require.Equal(t, "svcrs_runner", store.claim.ServiceRuntimeSessionID)
	require.Equal(t, "runner:svcrs_runner", store.claim.LeaseOwner)
	require.Equal(t, 25, store.claim.Limit)
	require.Equal(t, time.Minute, store.claim.LeaseDuration)
}

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

	for name, credential := range map[string]serviceactor.RuntimeCredential{
		"callback transport": func() serviceactor.RuntimeCredential {
			c := runnerCredential()
			c.TransportKind = serviceactor.RuntimeTransportKindCallback
			return c
		}(),
		"wrong kind": func() serviceactor.RuntimeCredential {
			c := runnerCredential()
			c.CredentialKind = "callback_token"
			return c
		}(),
		"wrong service": func() serviceactor.RuntimeCredential { c := runnerCredential(); c.ServiceKind = "agent"; return c }(),
	} {
		t.Run(name, func(t *testing.T) {
			t.Parallel()
			store := &fakeRunnerStore{credential: credential}
			service := NewRunnerService(RunnerServiceOptions{Store: store, Authority: &fakeRunnerAuthority{credential: store.credential}})
			_, err := service.AuthenticateRunner(context.Background(), "runner-token")
			require.ErrorIs(t, err, storeerr.ErrUnauthorized)
		})
	}
}

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

	now := time.Date(2026, 6, 1, 11, 0, 0, 0, time.UTC)
	store := &fakeRunnerStore{
		credential: runnerCredential(),
		loaded: serviceactor.RuntimeSession{
			ID: "svcrs_1", WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1", ServiceRuntimeCredentialID: "svcrc_1",
		},
	}
	service := NewRunnerService(RunnerServiceOptions{Store: store, Authority: &fakeRunnerAuthority{credential: store.credential}, Now: func() time.Time { return now }})
	auth, err := service.AuthenticateRunner(context.Background(), "runner-token")
	require.NoError(t, err)

	session, err := service.MarkSessionOffline(context.Background(), auth, "svcrs_1")
	require.NoError(t, err)
	require.Equal(t, serviceactor.RuntimeSessionStatusOffline, session.Status)
	require.Equal(t, "svcrs_1", store.statusID)
	require.Equal(t, serviceactor.RuntimeSessionStatusOffline, store.statusValue)
}

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

	store := &fakeRunnerStore{
		credential: runnerCredential(),
		loaded: serviceactor.RuntimeSession{
			ID: "svcrs_other", WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1", ServiceRuntimeCredentialID: "svcrc_other",
		},
	}
	service := NewRunnerService(RunnerServiceOptions{Store: store, Authority: &fakeRunnerAuthority{credential: store.credential}})
	auth, err := service.AuthenticateRunner(context.Background(), "runner-token")
	require.NoError(t, err)

	_, err = service.MarkSessionOffline(context.Background(), auth, "svcrs_other")
	require.ErrorIs(t, err, storeerr.ErrForbidden)
	require.Empty(t, store.statusID)
}

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

	expected := errors.New("credential backend unavailable")
	service := NewRunnerService(RunnerServiceOptions{
		Store:     &fakeRunnerStore{authErr: expected},
		Authority: &fakeRunnerAuthority{},
	})
	_, err := service.AuthenticateRunner(context.Background(), "runner-token")
	require.ErrorIs(t, err, expected)
}

func runnerCredential() serviceactor.RuntimeCredential {
	return serviceactor.RuntimeCredential{
		ID:                    "svcrc_1",
		WorkspaceID:           "wsp_1",
		ServiceDefinitionID:   "svcdef_1",
		ServiceInstallationID: "svcinst_1",
		ServiceMemberID:       "svcmem_1",
		ServiceKind:           serviceactor.KindApp,
		CredentialKind:        serviceactor.RuntimeCredentialKindRunnerStreamToken,
		TransportKind:         serviceactor.RuntimeTransportKindRunnerStream,
		Status:                serviceactor.RuntimeCredentialStatusActive,
	}
}
