package activity

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

	"github.com/tidbcloud/AgentTeam/internal/gormtest"

	"github.com/stretchr/testify/require"
	activitystore "github.com/tidbcloud/AgentTeam/internal/store/resource/activity"
	"github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
	"gorm.io/gorm"
)

type fakeActivityRepoLister struct {
	repos []repos.ResourceRepository
}

func (l fakeActivityRepoLister) ListActivityProjectorRepositories(_ context.Context, _ repos.ListActivityProjectorRepositoriesOptions) ([]repos.ResourceRepository, error) {
	return l.repos, nil
}

type recordingActivityRepoLister struct {
	repos    []repos.ResourceRepository
	lastOpts repos.ListActivityProjectorRepositoriesOptions
}

func (l *recordingActivityRepoLister) ListActivityProjectorRepositories(_ context.Context, opts repos.ListActivityProjectorRepositoriesOptions) ([]repos.ResourceRepository, error) {
	l.lastOpts = opts
	return l.repos, nil
}

type fakeActivitySource struct {
	eventsByRepoKind map[string][]ResourceActivityDataPlaneEvent
	errByRepoKind    map[string]error
}

func (s fakeActivitySource) ListActivityEvents(_ context.Context, repo ResourceActivityDataPlaneRepository, kind string, _ ResourceActivityDataPlaneListOptions) (ResourceActivityDataPlaneBatch, error) {
	key := repo.RepoFullName + "/" + kind
	if err := s.errByRepoKind[key]; err != nil {
		return ResourceActivityDataPlaneBatch{}, err
	}
	return ResourceActivityDataPlaneBatch{Events: s.eventsByRepoKind[key]}, nil
}

type recordingActivitySource struct {
	calls map[string]int
}

func (s *recordingActivitySource) ListActivityEvents(_ context.Context, repo ResourceActivityDataPlaneRepository, kind string, _ ResourceActivityDataPlaneListOptions) (ResourceActivityDataPlaneBatch, error) {
	if s.calls != nil {
		s.calls[repo.RepoFullName+"/"+kind]++
	}
	return ResourceActivityDataPlaneBatch{}, nil
}

func newActivityProjectorTestDB(t *testing.T) (*gorm.DB, *activitystore.Store) {
	t.Helper()
	db, err := gormtest.OpenSQLiteDB(t)
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&activitystore.ResourceActivityEvent{}, &activitystore.ResourceActivityLatestCommit{}, &activitystore.ResourceActivityProjectorCheckpoint{}))
	return db, activitystore.New(db)
}

func projectorTestRepo(resourceID string) repos.ResourceRepository {
	return repos.ResourceRepository{
		ID:           "rr_" + resourceID,
		WorkspaceID:  "ws_1",
		ResourceType: repos.ResourceTypeMemory,
		ResourceID:   resourceID,
		RepoRole:     repos.RepoRolePrimary,
		RepoOwner:    "owner",
		RepoName:     resourceID + ".mem",
		RepoFullName: "owner/" + resourceID + ".mem",
	}
}

func TestResourceActivityProjectorIsIdempotentBySourceEventID(t *testing.T) {
	ctx := context.Background()
	_, store := newActivityProjectorTestDB(t)
	now := time.Date(2026, 6, 22, 12, 0, 0, 0, time.UTC)
	repo := projectorTestRepo("mem_1")
	sourceEventID := "gitea:owner/mem_1.mem:commit:abc"
	projector := &ResourceActivityProjector{
		Repositories: fakeActivityRepoLister{repos: []repos.ResourceRepository{repo}},
		Activity:     store,
		Source: fakeActivitySource{eventsByRepoKind: map[string][]ResourceActivityDataPlaneEvent{
			repo.RepoFullName + "/" + activitystore.ProjectorKindCommits: {{
				SourceEventID: sourceEventID,
				EventType:     "commit_pushed",
				TargetKind:    "commit",
				TargetID:      "abc",
				OccurredAt:    now,
			}},
		}},
		Now: func() time.Time { return now.Add(time.Minute) },
	}
	first, err := projector.RunOnce(ctx, ResourceActivityProjectorRunOptions{})
	require.NoError(t, err)
	require.Equal(t, 1, first.Events)
	second, err := projector.RunOnce(ctx, ResourceActivityProjectorRunOptions{})
	require.NoError(t, err)
	require.Equal(t, 0, second.Events)

	page, err := store.ListEvents(ctx, activitystore.ListOptions{WorkspaceID: "ws_1", AssetType: activitystore.AssetTypeMemory, VisibleAssetIDs: []string{"mem_1"}, Limit: 10})
	require.NoError(t, err)
	require.Len(t, page.Items, 1)
	require.Equal(t, sourceEventID, page.Items[0].SourceEventIDValue())
}

func TestResourceActivityProjectorDerivesSelectorsFromProjectionRules(t *testing.T) {
	ctx := context.Background()
	_, store := newActivityProjectorTestDB(t)
	lister := &recordingActivityRepoLister{repos: []repos.ResourceRepository{projectorTestRepo("mem_1")}}
	projector := &ResourceActivityProjector{
		Repositories: lister,
		Activity:     store,
		Source:       &recordingActivitySource{calls: map[string]int{}},
		Now:          func() time.Time { return time.Date(2026, 6, 22, 12, 0, 0, 0, time.UTC) },
	}
	_, err := projector.RunOnce(ctx, ResourceActivityProjectorRunOptions{})
	require.NoError(t, err)
	require.Equal(t, []repos.ActivityProjectorRepositorySelector{
		{ResourceType: repos.ResourceTypeMemory, RepoRoles: []string{repos.RepoRolePrimary}},
		{ResourceType: repos.ResourceTypeSkillRepository, RepoRoles: []string{repos.RepoRolePrimary}},
		{ResourceType: repos.ResourceTypeKBSource, RepoRoles: []string{repos.RepoRolePrimary, repos.RepoRoleMirror, repos.RepoRoleInsights}},
	}, lister.lastOpts.Selectors)
}

func TestResourceActivityProjectorProjectsDefaultKindsPerResourceType(t *testing.T) {
	ctx := context.Background()
	_, store := newActivityProjectorTestDB(t)
	memoryRepo := repos.ResourceRepository{ID: "rr_memory", WorkspaceID: "ws_1", ResourceType: repos.ResourceTypeMemory, ResourceID: "mem_1", RepoRole: repos.RepoRolePrimary, RepoOwner: "owner", RepoName: "mem_1.mem", RepoFullName: "owner/mem_1.mem"}
	skillRepo := repos.ResourceRepository{ID: "rr_skill", WorkspaceID: "ws_1", ResourceType: repos.ResourceTypeSkillRepository, ResourceID: "skill_1", RepoRole: repos.RepoRolePrimary, RepoOwner: "owner", RepoName: "skill_1.sk", RepoFullName: "owner/skill_1.sk"}
	kbRepo := repos.ResourceRepository{ID: "rr_kb", WorkspaceID: "ws_1", ResourceType: repos.ResourceTypeKBSource, ResourceID: "src_1", RepoRole: repos.RepoRolePrimary, RepoOwner: "owner", RepoName: "src_1.kb", RepoFullName: "owner/src_1.kb"}
	insightsRepo := repos.ResourceRepository{ID: "rr_kb_insights", WorkspaceID: "ws_1", ResourceType: repos.ResourceTypeKBSource, ResourceID: "src_1", RepoRole: repos.RepoRoleInsights, RepoOwner: "owner.kb", RepoName: "src_1", RepoFullName: "owner.kb/src_1"}
	source := &recordingActivitySource{calls: map[string]int{}}
	projector := &ResourceActivityProjector{
		Repositories: fakeActivityRepoLister{repos: []repos.ResourceRepository{memoryRepo, skillRepo, kbRepo, insightsRepo}},
		Activity:     store,
		Source:       source,
		Now:          func() time.Time { return time.Date(2026, 6, 22, 12, 0, 0, 0, time.UTC) },
	}
	_, err := projector.RunOnce(ctx, ResourceActivityProjectorRunOptions{})
	require.NoError(t, err)
	require.Equal(t, 1, source.calls[memoryRepo.RepoFullName+"/"+activitystore.ProjectorKindCommits])
	require.Equal(t, 1, source.calls[memoryRepo.RepoFullName+"/"+activitystore.ProjectorKindReleases])
	require.Equal(t, 0, source.calls[memoryRepo.RepoFullName+"/"+activitystore.ProjectorKindIssues])
	require.Equal(t, 0, source.calls[memoryRepo.RepoFullName+"/"+activitystore.ProjectorKindPulls])
	require.Equal(t, 0, source.calls[memoryRepo.RepoFullName+"/"+activitystore.ProjectorKindReviews])
	require.Equal(t, 0, source.calls[memoryRepo.RepoFullName+"/"+activitystore.ProjectorKindTags])
	require.Equal(t, 1, source.calls[skillRepo.RepoFullName+"/"+activitystore.ProjectorKindCommits])
	require.Equal(t, 1, source.calls[skillRepo.RepoFullName+"/"+activitystore.ProjectorKindReleases])
	require.Equal(t, 1, source.calls[kbRepo.RepoFullName+"/"+activitystore.ProjectorKindCommits])
	require.Equal(t, 1, source.calls[kbRepo.RepoFullName+"/"+activitystore.ProjectorKindReleases])
	require.Equal(t, 1, source.calls[insightsRepo.RepoFullName+"/"+activitystore.ProjectorKindWiki])
	require.Equal(t, 0, source.calls[insightsRepo.RepoFullName+"/"+activitystore.ProjectorKindCommits])
	require.Equal(t, 0, source.calls[insightsRepo.RepoFullName+"/"+activitystore.ProjectorKindReleases])
}

func TestResourceActivityProjectorSkipsFreshRefreshWithinDefaultTTL(t *testing.T) {
	ctx := context.Background()
	_, store := newActivityProjectorTestDB(t)
	now := time.Date(2026, 6, 22, 12, 0, 0, 0, time.UTC)
	repo := projectorTestRepo("mem_1")
	source := &recordingActivitySource{calls: map[string]int{}}
	projector := &ResourceActivityProjector{
		Activity: store,
		Source:   source,
		Now:      func() time.Time { return now },
	}
	identity := activitystore.ProjectorCheckpointIdentity{
		WorkspaceID:  "ws_1",
		ResourceType: repos.ResourceTypeMemory,
		ResourceID:   "mem_1",
		AssetType:    activitystore.AssetTypeMemory,
		AssetID:      "mem_1",
		RepoRole:     repos.RepoRolePrimary,
		RepoFullName: repo.RepoFullName,
		EventKind:    activitystore.ProjectorKindCommits,
	}
	_, err := store.MarkProjectorCheckpointSuccess(ctx, identity, activitystore.ProjectorCheckpointUpdate{LastPolledAt: now.Add(-2 * time.Minute)})
	require.NoError(t, err)
	result, err := projector.RefreshRepositories(ctx, []repos.ResourceRepository{repo}, ResourceActivityProjectorRefreshOptions{RefreshAfter: DefaultResourceActivityRefreshAfter})
	require.NoError(t, err)
	require.Equal(t, 0, result.Events)
	require.Equal(t, 0, source.calls[repo.RepoFullName+"/"+activitystore.ProjectorKindCommits])

	_, err = store.MarkProjectorCheckpointSuccess(ctx, identity, activitystore.ProjectorCheckpointUpdate{LastPolledAt: now.Add(-16 * time.Minute)})
	require.NoError(t, err)
	result, err = projector.RefreshRepositories(ctx, []repos.ResourceRepository{repo}, ResourceActivityProjectorRefreshOptions{RefreshAfter: DefaultResourceActivityRefreshAfter})
	require.NoError(t, err)
	require.Equal(t, 0, result.Errors)
	require.Equal(t, 1, source.calls[repo.RepoFullName+"/"+activitystore.ProjectorKindCommits])
}

func TestResourceActivityProjectorCommitCheckpointsAreRefScoped(t *testing.T) {
	ctx := context.Background()
	_, store := newActivityProjectorTestDB(t)
	now := time.Date(2026, 6, 22, 12, 0, 0, 0, time.UTC)
	mainRepo := projectorTestRepo("mem_1")
	mainRepo.DefaultRef = "main"
	agentRepo := mainRepo
	agentRepo.DefaultRef = "agent/alice"
	mainSeenAt := now
	agentCommitAt := now.Add(-time.Hour)
	mainIdentity := activitystore.ProjectorCheckpointIdentity{
		WorkspaceID:  "ws_1",
		ResourceType: repos.ResourceTypeMemory,
		ResourceID:   "mem_1",
		AssetType:    activitystore.AssetTypeMemory,
		AssetID:      "mem_1",
		RepoRole:     repos.RepoRolePrimary,
		RepoFullName: mainRepo.RepoFullName,
		EventKind:    resourceActivityProjectorCheckpointEventKind(ResourceActivityDataPlaneRepository{DefaultRef: mainRepo.DefaultRef}, activitystore.ProjectorKindCommits),
	}
	agentIdentity := mainIdentity
	agentIdentity.EventKind = resourceActivityProjectorCheckpointEventKind(ResourceActivityDataPlaneRepository{DefaultRef: agentRepo.DefaultRef}, activitystore.ProjectorKindCommits)
	_, err := store.MarkProjectorCheckpointSuccess(ctx, mainIdentity, activitystore.ProjectorCheckpointUpdate{
		LastSeenAt:        &mainSeenAt,
		LastSourceEventID: "gitea:owner/mem_1.mem:commit:main:new",
		LastPolledAt:      now,
	})
	require.NoError(t, err)
	_, err = store.MarkProjectorCheckpointSuccess(ctx, agentIdentity, activitystore.ProjectorCheckpointUpdate{
		LastPolledAt: now.Add(-time.Minute),
	})
	require.NoError(t, err)

	source := fakeActivitySource{eventsByRepoKind: map[string][]ResourceActivityDataPlaneEvent{
		agentRepo.RepoFullName + "/" + activitystore.ProjectorKindCommits: {{
			SourceEventID: "gitea:owner/mem_1.mem:commit:agent/alice:old",
			EventType:     "commit_pushed",
			TargetKind:    "commit",
			TargetID:      "old",
			OccurredAt:    agentCommitAt,
			Details:       map[string]any{"ref": "agent/alice"},
		}},
	}}
	projector := &ResourceActivityProjector{Activity: store, Source: source, Now: func() time.Time { return now }}

	result, err := projector.RefreshRepositories(ctx, []repos.ResourceRepository{agentRepo}, ResourceActivityProjectorRefreshOptions{Force: true})
	require.NoError(t, err)
	require.Equal(t, 1, result.Events)

	checkpoint, err := store.GetProjectorCheckpoint(ctx, agentIdentity)
	require.NoError(t, err)
	require.NotNil(t, checkpoint.LastSeenAt)
	require.Equal(t, agentCommitAt, *checkpoint.LastSeenAt)
}

func TestResourceActivityProjectorAdvancesCursorIncrementally(t *testing.T) {
	ctx := context.Background()
	_, store := newActivityProjectorTestDB(t)
	base := time.Date(2026, 6, 22, 12, 0, 0, 0, time.UTC)
	repo := projectorTestRepo("mem_1")
	source := fakeActivitySource{eventsByRepoKind: map[string][]ResourceActivityDataPlaneEvent{
		repo.RepoFullName + "/" + activitystore.ProjectorKindCommits: {
			{SourceEventID: "gitea:owner/mem_1.mem:commit:a", EventType: "commit_pushed", TargetKind: "commit", TargetID: "a", OccurredAt: base},
			{SourceEventID: "gitea:owner/mem_1.mem:commit:b", EventType: "commit_pushed", TargetKind: "commit", TargetID: "b", OccurredAt: base.Add(time.Minute)},
		},
	}}
	projector := &ResourceActivityProjector{Repositories: fakeActivityRepoLister{repos: []repos.ResourceRepository{repo}}, Activity: store, Source: source, Now: func() time.Time { return base.Add(2 * time.Minute) }}
	result, err := projector.RunOnce(ctx, ResourceActivityProjectorRunOptions{})
	require.NoError(t, err)
	require.Equal(t, 2, result.Events)

	source.eventsByRepoKind[repo.RepoFullName+"/"+activitystore.ProjectorKindCommits] = append(source.eventsByRepoKind[repo.RepoFullName+"/"+activitystore.ProjectorKindCommits],
		ResourceActivityDataPlaneEvent{SourceEventID: "gitea:owner/mem_1.mem:commit:c", EventType: "commit_pushed", TargetKind: "commit", TargetID: "c", OccurredAt: base.Add(2 * time.Minute)})
	projector.Source = source
	result, err = projector.RunOnce(ctx, ResourceActivityProjectorRunOptions{})
	require.NoError(t, err)
	require.Equal(t, 1, result.Events)

	checkpoint, err := store.GetProjectorCheckpoint(ctx, activitystore.ProjectorCheckpointIdentity{WorkspaceID: "ws_1", ResourceType: repos.ResourceTypeMemory, ResourceID: "mem_1", RepoRole: repos.RepoRolePrimary, EventKind: activitystore.ProjectorKindCommits})
	require.NoError(t, err)
	require.NotNil(t, checkpoint.LastSeenAt)
	require.Equal(t, base.Add(2*time.Minute), *checkpoint.LastSeenAt)
	require.Equal(t, "gitea:owner/mem_1.mem:commit:c", checkpoint.LastSourceEventID)
}

func TestResourceActivityProjectorRecordsMissingRepoAndContinues(t *testing.T) {
	ctx := context.Background()
	_, store := newActivityProjectorTestDB(t)
	now := time.Date(2026, 6, 22, 12, 0, 0, 0, time.UTC)
	missing := projectorTestRepo("mem_missing")
	okRepo := projectorTestRepo("mem_ok")
	projector := &ResourceActivityProjector{
		Repositories: fakeActivityRepoLister{repos: []repos.ResourceRepository{missing, okRepo}},
		Activity:     store,
		Source: fakeActivitySource{
			errByRepoKind: map[string]error{
				missing.RepoFullName + "/" + activitystore.ProjectorKindCommits: fmtError(ErrResourceActivityProjectorNotFound),
			},
			eventsByRepoKind: map[string][]ResourceActivityDataPlaneEvent{
				okRepo.RepoFullName + "/" + activitystore.ProjectorKindCommits: {{
					SourceEventID: "gitea:owner/mem_ok.mem:commit:a",
					EventType:     "commit_pushed",
					TargetKind:    "commit",
					TargetID:      "a",
					OccurredAt:    now,
				}},
			},
		},
		Now: func() time.Time { return now.Add(time.Minute) },
	}
	result, err := projector.RunOnce(ctx, ResourceActivityProjectorRunOptions{})
	require.NoError(t, err)
	require.Equal(t, 1, result.Errors)
	require.Equal(t, 1, result.Events)

	checkpoint, err := store.GetProjectorCheckpoint(ctx, activitystore.ProjectorCheckpointIdentity{WorkspaceID: "ws_1", ResourceType: repos.ResourceTypeMemory, ResourceID: "mem_missing", RepoRole: repos.RepoRolePrimary, EventKind: activitystore.ProjectorKindCommits})
	require.NoError(t, err)
	require.Equal(t, activitystore.ProjectorStatusError, checkpoint.Status)
	require.Contains(t, checkpoint.LastError, ErrResourceActivityProjectorNotFound.Error())
}

func TestResourceActivityProjectorRecordsRateLimitStatus(t *testing.T) {
	ctx := context.Background()
	_, store := newActivityProjectorTestDB(t)
	now := time.Date(2026, 6, 22, 12, 0, 0, 0, time.UTC)
	repo := projectorTestRepo("mem_limited")
	projector := &ResourceActivityProjector{
		Repositories: fakeActivityRepoLister{repos: []repos.ResourceRepository{repo}},
		Activity:     store,
		Source: fakeActivitySource{errByRepoKind: map[string]error{
			repo.RepoFullName + "/" + activitystore.ProjectorKindCommits: fmt.Errorf("%w: fake", ErrResourceActivityProjectorRateLimited),
		}},
		Now: func() time.Time { return now },
	}
	result, err := projector.RunOnce(ctx, ResourceActivityProjectorRunOptions{})
	require.NoError(t, err)
	require.Equal(t, 1, result.Errors)
	checkpoint, err := store.GetProjectorCheckpoint(ctx, activitystore.ProjectorCheckpointIdentity{WorkspaceID: "ws_1", ResourceType: repos.ResourceTypeMemory, ResourceID: "mem_limited", RepoRole: repos.RepoRolePrimary, EventKind: activitystore.ProjectorKindCommits})
	require.NoError(t, err)
	require.Equal(t, activitystore.ProjectorStatusRateLimited, checkpoint.Status)
	require.Contains(t, checkpoint.LastError, ErrResourceActivityProjectorRateLimited.Error())
}

func TestResourceActivityProjectorAppliesBackfillWindowAndPerKindLimit(t *testing.T) {
	ctx := context.Background()
	_, store := newActivityProjectorTestDB(t)
	now := time.Date(2026, 6, 22, 12, 0, 0, 0, time.UTC)
	repo := projectorTestRepo("mem_1")
	projector := &ResourceActivityProjector{
		Repositories: fakeActivityRepoLister{repos: []repos.ResourceRepository{repo}},
		Activity:     store,
		Config:       ResourceActivityProjectorConfig{BackfillWindow: 90 * 24 * time.Hour, PerKindLimit: 2},
		Source: fakeActivitySource{eventsByRepoKind: map[string][]ResourceActivityDataPlaneEvent{
			repo.RepoFullName + "/" + activitystore.ProjectorKindCommits: {
				{SourceEventID: "old", EventType: "commit_pushed", TargetKind: "commit", TargetID: "old", OccurredAt: now.Add(-100 * 24 * time.Hour)},
				{SourceEventID: "recent-1", EventType: "commit_pushed", TargetKind: "commit", TargetID: "recent-1", OccurredAt: now.Add(-3 * time.Hour)},
				{SourceEventID: "recent-2", EventType: "commit_pushed", TargetKind: "commit", TargetID: "recent-2", OccurredAt: now.Add(-2 * time.Hour)},
				{SourceEventID: "recent-3", EventType: "commit_pushed", TargetKind: "commit", TargetID: "recent-3", OccurredAt: now.Add(-1 * time.Hour)},
			},
		}},
		Now: func() time.Time { return now },
	}
	result, err := projector.RunOnce(ctx, ResourceActivityProjectorRunOptions{})
	require.NoError(t, err)
	require.Equal(t, 2, result.Events)
	page, err := store.ListEvents(ctx, activitystore.ListOptions{WorkspaceID: "ws_1", AssetType: activitystore.AssetTypeMemory, VisibleAssetIDs: []string{"mem_1"}, Limit: 10})
	require.NoError(t, err)
	require.Len(t, page.Items, 2)
	require.Equal(t, []string{"recent-3", "recent-2"}, []string{page.Items[0].SourceEventIDValue(), page.Items[1].SourceEventIDValue()})
}

func TestResourceActivityProjectorWritesWithoutGrantButReadSideFilters(t *testing.T) {
	ctx := context.Background()
	_, store := newActivityProjectorTestDB(t)
	now := time.Date(2026, 6, 22, 12, 0, 0, 0, time.UTC)
	repo := projectorTestRepo("mem_private")
	projector := &ResourceActivityProjector{
		Repositories: fakeActivityRepoLister{repos: []repos.ResourceRepository{repo}},
		Activity:     store,
		Source: fakeActivitySource{eventsByRepoKind: map[string][]ResourceActivityDataPlaneEvent{
			repo.RepoFullName + "/" + activitystore.ProjectorKindCommits: {{
				SourceEventID: "private-commit",
				EventType:     "commit_pushed",
				TargetKind:    "commit",
				TargetID:      "abc",
				OccurredAt:    now,
			}},
		}},
		Now: func() time.Time { return now },
	}
	result, err := projector.RunOnce(ctx, ResourceActivityProjectorRunOptions{})
	require.NoError(t, err)
	require.Equal(t, 1, result.Events)

	page, err := ListResourceActivity(ctx, store, "ws_1", ResourceActivityDescriptor{
		AssetType:     activitystore.AssetTypeMemory,
		VisibleAssets: []ResourceActivityVisibleAsset{},
	}, ResourceActivityListOptions{Limit: 10})
	require.NoError(t, err)
	require.Empty(t, page.Items)
}

func TestAGSHTTPResourceActivitySourceProjectsTagDeletionFromCheckpointState(t *testing.T) {
	now := time.Date(2026, 6, 22, 12, 0, 0, 0, time.UTC)
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, "/api/v3/repos/owner/repo/tags", r.URL.Path)
		_, _ = w.Write([]byte(`[{"name":"v2","commit":{"sha":"abcdef1234567890"},"html_url":"https://gitea/owner/repo/releases/tag/v2"}]`))
	})
	source := AGSHTTPResourceActivitySource{Handler: handler}
	repo := ResourceActivityDataPlaneRepository{RepoOwner: "owner", RepoName: "repo", RepoFullName: "owner/repo"}
	batch, err := source.ListActivityEvents(context.Background(), repo, activitystore.ProjectorKindTags, ResourceActivityDataPlaneListOptions{StateJSON: `{"tags":["v1","v2"]}`, Now: now})
	require.NoError(t, err)
	require.Len(t, batch.Events, 1)
	require.Equal(t, "tag_deleted", batch.Events[0].EventType)
	require.Equal(t, "v1", batch.Events[0].TargetID)
	require.JSONEq(t, `{"tags":["v2"]}`, batch.StateJSON)
}

func fmtError(err error) error {
	return errors.New(err.Error() + ": fake")
}
