package activity

import (
	"context"
	"encoding/base64"
	"errors"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func newActivityStoreForTest(t *testing.T) *Store {
	t.Helper()
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&ResourceActivityEvent{}, &ResourceActivityLatestCommit{}, &ResourceActivityProjectorCheckpoint{}))
	return New(db)
}

func strptr(value string) *string { return &value }

func TestUpsertEventMaintainsLatestCommit(t *testing.T) {
	ctx := context.Background()
	store := newActivityStoreForTest(t)
	base := time.Date(2026, 7, 31, 10, 0, 0, 0, time.UTC)
	for _, event := range []ResourceActivityEvent{
		{ID: "act_new", WorkspaceID: "ws_1", AssetType: AssetTypeKB, AssetID: "src_1", EventType: EventTypeCommit, OccurredAt: base.Add(time.Hour)},
		{ID: "act_old", WorkspaceID: "ws_1", AssetType: AssetTypeKB, AssetID: "src_1", EventType: EventTypeCommit, OccurredAt: base},
	} {
		_, err := store.UpsertEvent(ctx, event)
		require.NoError(t, err)
	}
	var latest ResourceActivityLatestCommit
	require.NoError(t, store.db.First(&latest, "workspace_id = ? AND asset_type = ? AND asset_id = ?", "ws_1", AssetTypeKB, "src_1").Error)
	require.Equal(t, "act_new", latest.EventID)
	require.Equal(t, base.Add(time.Hour), latest.OccurredAt)
}

func TestUpsertEventLatestCommitUsesEventIDAsTimestampTieBreak(t *testing.T) {
	ctx := context.Background()
	store := newActivityStoreForTest(t)
	occurredAt := time.Date(2026, 7, 31, 10, 0, 0, 0, time.UTC)
	for _, id := range []string{"act_b", "act_a", "act_c"} {
		_, err := store.UpsertEvent(ctx, ResourceActivityEvent{ID: id, WorkspaceID: "ws_1", AssetType: AssetTypeKB, AssetID: "src_1", EventType: EventTypeCommit, OccurredAt: occurredAt})
		require.NoError(t, err)
	}
	var latest ResourceActivityLatestCommit
	require.NoError(t, store.db.First(&latest, "workspace_id = ? AND asset_type = ? AND asset_id = ?", "ws_1", AssetTypeKB, "src_1").Error)
	require.Equal(t, "act_c", latest.EventID)
	require.Equal(t, occurredAt, latest.OccurredAt)
}

func TestUpsertEventProjectsOnlyCommits(t *testing.T) {
	ctx := context.Background()
	store := newActivityStoreForTest(t)
	_, err := store.UpsertEvent(ctx, ResourceActivityEvent{ID: "act_issue", WorkspaceID: "ws_1", AssetType: AssetTypeKB, AssetID: "src_1", EventType: "issue_opened", OccurredAt: time.Now().UTC()})
	require.NoError(t, err)
	var count int64
	require.NoError(t, store.db.Model(&ResourceActivityLatestCommit{}).Count(&count).Error)
	require.Zero(t, count)
}

func TestUpsertEventReplayDoesNotChangeLatestCommit(t *testing.T) {
	ctx := context.Background()
	store := newActivityStoreForTest(t)
	sourceEventID := "commit:abc"
	firstOccurredAt := time.Date(2026, 7, 31, 10, 0, 0, 0, time.UTC)
	first, err := store.UpsertEvent(ctx, ResourceActivityEvent{ID: "act_first", WorkspaceID: "ws_1", AssetType: AssetTypeKB, AssetID: "src_1", EventType: EventTypeCommit, Source: SourceGitea, SourceEventID: &sourceEventID, OccurredAt: firstOccurredAt})
	require.NoError(t, err)
	replayed, err := store.UpsertEvent(ctx, ResourceActivityEvent{ID: "act_replayed", WorkspaceID: "ws_1", AssetType: AssetTypeKB, AssetID: "src_1", EventType: EventTypeCommit, Source: SourceGitea, SourceEventID: &sourceEventID, OccurredAt: firstOccurredAt.Add(time.Hour)})
	require.NoError(t, err)
	require.Equal(t, first.ID, replayed.ID)
	var latest ResourceActivityLatestCommit
	require.NoError(t, store.db.First(&latest, "workspace_id = ? AND asset_type = ? AND asset_id = ?", "ws_1", AssetTypeKB, "src_1").Error)
	require.Equal(t, first.ID, latest.EventID)
	require.Equal(t, firstOccurredAt, latest.OccurredAt)
}

func TestUpsertEventRollsBackCommitHistoryWhenProjectionFails(t *testing.T) {
	store := newActivityStoreForTest(t)
	require.NoError(t, store.db.Migrator().DropTable(&ResourceActivityLatestCommit{}))
	event := ResourceActivityEvent{ID: "act_rollback", WorkspaceID: "ws_1", AssetType: AssetTypeKB, AssetID: "src_1", EventType: EventTypeCommit, OccurredAt: time.Now().UTC()}
	_, err := store.UpsertEvent(context.Background(), event)
	require.Error(t, err)
	var count int64
	require.NoError(t, store.db.Model(&ResourceActivityEvent{}).Where("id = ?", event.ID).Count(&count).Error)
	require.Zero(t, count)
}

func TestListEventsCursorOrderAndFilters(t *testing.T) {
	ctx := context.Background()
	store := newActivityStoreForTest(t)
	base := time.Date(2026, 6, 22, 10, 0, 0, 0, time.UTC)
	events := []ResourceActivityEvent{
		{ID: "act_1", WorkspaceID: "ws_1", AssetType: AssetTypeMemory, AssetID: "mem_1", EventType: "created", ActorID: "usr_1", OccurredAt: base.Add(-3 * time.Minute)},
		{ID: "act_2", WorkspaceID: "ws_1", AssetType: AssetTypeMemory, AssetID: "mem_2", EventType: "updated", ActorID: "usr_2", OccurredAt: base.Add(-2 * time.Minute)},
		{ID: "act_3", WorkspaceID: "ws_1", AssetType: AssetTypeMemory, AssetID: "mem_1", EventType: "updated", ActorID: "usr_1", OccurredAt: base.Add(-1 * time.Minute)},
		{ID: "act_4", WorkspaceID: "ws_1", AssetType: AssetTypeSkill, AssetID: "skill_1", EventType: "updated", ActorID: "usr_1", OccurredAt: base},
	}
	for _, event := range events {
		_, err := store.UpsertEvent(ctx, event)
		require.NoError(t, err)
	}

	page, err := store.ListEvents(ctx, ListOptions{WorkspaceID: "ws_1", AssetType: AssetTypeMemory, VisibleAssetIDs: []string{"mem_1", "mem_2"}, Limit: 2})
	require.NoError(t, err)
	require.Len(t, page.Items, 2)
	require.Equal(t, []string{"act_3", "act_2"}, []string{page.Items[0].ID, page.Items[1].ID})
	require.NotEmpty(t, page.NextCursor)

	next, err := store.ListEvents(ctx, ListOptions{WorkspaceID: "ws_1", AssetType: AssetTypeMemory, VisibleAssetIDs: []string{"mem_1", "mem_2"}, Cursor: page.NextCursor, Limit: 2})
	require.NoError(t, err)
	require.Len(t, next.Items, 1)
	require.Equal(t, "act_1", next.Items[0].ID)
	require.Empty(t, next.NextCursor)

	filtered, err := store.ListEvents(ctx, ListOptions{WorkspaceID: "ws_1", AssetType: AssetTypeMemory, VisibleAssetIDs: []string{"mem_1", "mem_2"}, AssetID: "mem_1", EventType: "updated", ActorID: "usr_1", Limit: 10})
	require.NoError(t, err)
	require.Len(t, filtered.Items, 1)
	require.Equal(t, "act_3", filtered.Items[0].ID)
}

func TestListEventsRequiresVisibleAssetIDs(t *testing.T) {
	ctx := context.Background()
	store := newActivityStoreForTest(t)
	_, err := store.UpsertEvent(ctx, ResourceActivityEvent{ID: "act_1", WorkspaceID: "ws_1", AssetType: AssetTypeMemory, AssetID: "mem_1", EventType: "created", OccurredAt: time.Now().UTC()})
	require.NoError(t, err)

	page, err := store.ListEvents(ctx, ListOptions{WorkspaceID: "ws_1", AssetType: AssetTypeMemory, VisibleAssetIDs: nil, Limit: 10})
	require.NoError(t, err)
	require.Empty(t, page.Items)
}

func TestListEventsFiltersByAssetIDs(t *testing.T) {
	ctx := context.Background()
	store := newActivityStoreForTest(t)
	now := time.Date(2026, 6, 22, 10, 0, 0, 0, time.UTC)
	for _, event := range []ResourceActivityEvent{
		{ID: "act_src_1", WorkspaceID: "ws_1", AssetType: AssetTypeKB, AssetID: "src_1", EventType: "commit_pushed", OccurredAt: now.Add(2 * time.Minute)},
		{ID: "act_src_2", WorkspaceID: "ws_1", AssetType: AssetTypeKB, AssetID: "src_2", EventType: "wiki_page_updated", OccurredAt: now.Add(time.Minute)},
		{ID: "act_src_3", WorkspaceID: "ws_1", AssetType: AssetTypeKB, AssetID: "src_3", EventType: "commit_pushed", OccurredAt: now},
	} {
		_, err := store.UpsertEvent(ctx, event)
		require.NoError(t, err)
	}

	page, err := store.ListEvents(ctx, ListOptions{
		WorkspaceID:     "ws_1",
		AssetType:       AssetTypeKB,
		VisibleAssetIDs: []string{"src_1", "src_2", "src_3"},
		AssetIDs:        []string{"src_1", "src_2"},
		Limit:           10,
	})

	require.NoError(t, err)
	require.Equal(t, []string{"act_src_1", "act_src_2"}, []string{page.Items[0].ID, page.Items[1].ID})
}

func TestUpsertEventGeneratesActivityID(t *testing.T) {
	ctx := context.Background()
	store := newActivityStoreForTest(t)

	event, err := store.UpsertEvent(ctx, ResourceActivityEvent{WorkspaceID: "ws_1", AssetType: AssetTypeMemory, AssetID: "mem_1", EventType: "created", OccurredAt: time.Now().UTC()})

	require.NoError(t, err)
	require.Regexp(t, `^act_[a-z0-9]{14}$`, event.ID)
}

func TestListEventsIncludesActorTombstonesWithoutVisibleAssetIDs(t *testing.T) {
	ctx := context.Background()
	store := newActivityStoreForTest(t)
	base := time.Date(2026, 6, 22, 10, 0, 0, 0, time.UTC)
	events := []ResourceActivityEvent{
		{ID: "act_actor_deleted", WorkspaceID: "ws_1", AssetType: AssetTypeMemory, AssetID: "mem_deleted", EventType: "deleted", ActorID: "usr_1", OccurredAt: base.Add(3 * time.Minute)},
		{ID: "act_actor_updated", WorkspaceID: "ws_1", AssetType: AssetTypeMemory, AssetID: "mem_deleted", EventType: "updated", ActorID: "usr_1", OccurredAt: base.Add(2 * time.Minute)},
		{ID: "act_other_deleted", WorkspaceID: "ws_1", AssetType: AssetTypeMemory, AssetID: "mem_other", EventType: "deleted", ActorID: "usr_2", OccurredAt: base.Add(time.Minute)},
	}
	for _, event := range events {
		_, err := store.UpsertEvent(ctx, event)
		require.NoError(t, err)
	}

	page, err := store.ListEvents(ctx, ListOptions{
		WorkspaceID:         "ws_1",
		AssetType:           AssetTypeMemory,
		TombstoneActorID:    "usr_1",
		TombstoneEventTypes: []string{"archived", "deleted"},
		Limit:               10,
	})

	require.NoError(t, err)
	require.Len(t, page.Items, 1)
	require.Equal(t, "act_actor_deleted", page.Items[0].ID)
}

func TestUpsertEventIsIdempotentBySourceEventID(t *testing.T) {
	ctx := context.Background()
	store := newActivityStoreForTest(t)
	sourceEventID := "gitea:repo:issue:1:opened"
	first, err := store.UpsertEvent(ctx, ResourceActivityEvent{ID: "act_first", WorkspaceID: "ws_1", AssetType: AssetTypeSkill, AssetID: "skill_1", EventType: "issue_opened", Source: SourceGitea, SourceEventID: &sourceEventID, OccurredAt: time.Now().UTC()})
	require.NoError(t, err)
	second, err := store.UpsertEvent(ctx, ResourceActivityEvent{ID: "act_second", WorkspaceID: "ws_1", AssetType: AssetTypeSkill, AssetID: "skill_1", EventType: "issue_opened", Source: SourceGitea, SourceEventID: &sourceEventID, OccurredAt: time.Now().UTC().Add(time.Minute)})
	require.NoError(t, err)
	require.Equal(t, first.ID, second.ID)
	require.Equal(t, "act_first", second.ID)

	page, err := store.ListEvents(ctx, ListOptions{WorkspaceID: "ws_1", AssetType: AssetTypeSkill, VisibleAssetIDs: []string{"skill_1"}, Limit: 10})
	require.NoError(t, err)
	require.Len(t, page.Items, 1)
}

func TestDecodeCursorRejectsInvalidInput(t *testing.T) {
	_, err := DecodeCursor("not-base64")
	require.True(t, errors.Is(err, ErrInvalidCursor))
}

func TestCursorCodecPreservesOpaquePayload(t *testing.T) {
	occurredAt := time.Date(2026, 6, 22, 10, 0, 0, 123456789, time.FixedZone("offset", 8*60*60))

	encoded, err := EncodeCursor(occurredAt, " act_1 ")
	require.NoError(t, err)
	require.Equal(t, base64.RawURLEncoding.EncodeToString([]byte(`{"occurredAt":"2026-06-22T02:00:00.123456789Z","id":"act_1"}`)), encoded)

	decoded, err := DecodeCursor(encoded)
	require.NoError(t, err)
	require.Equal(t, occurredAt.UTC(), decoded.OccurredAt)
	require.Equal(t, "2026-06-22T02:00:00.123456789Z", decoded.Occurred)
	require.Equal(t, "act_1", decoded.ID)
}

func TestProjectorCheckpointRecordsSuccessAndErrorState(t *testing.T) {
	ctx := context.Background()
	store := newActivityStoreForTest(t)
	identity := ProjectorCheckpointIdentity{
		WorkspaceID:  "ws_1",
		ResourceType: "memory",
		ResourceID:   "mem_1",
		AssetType:    AssetTypeMemory,
		AssetID:      "mem_1",
		RepoRole:     "primary",
		RepoFullName: "owner/memory.mem",
		EventKind:    ProjectorKindCommits,
	}
	seenAt := time.Date(2026, 6, 22, 10, 0, 0, 0, time.UTC)
	polledAt := seenAt.Add(time.Minute)
	success, err := store.MarkProjectorCheckpointSuccess(ctx, identity, ProjectorCheckpointUpdate{
		LastSeenAt:        &seenAt,
		LastSourceEventID: "gitea:owner/memory.mem:commit:abc",
		StateJSON:         `{"knownTags":["v1"]}`,
		LastPolledAt:      polledAt,
	})
	require.NoError(t, err)
	require.Regexp(t, `^rac_[a-z0-9]{14}$`, success.ID)
	require.Equal(t, ProjectorStatusOK, success.Status)
	require.Equal(t, 0, success.ConsecutiveErrors)
	require.Equal(t, `{"knownTags":["v1"]}`, success.StateJSON)
	require.NotNil(t, success.LastSeenAt)
	require.Equal(t, seenAt, *success.LastSeenAt)

	errorAt := polledAt.Add(time.Minute)
	failed, err := store.MarkProjectorCheckpointError(ctx, identity, ProjectorCheckpointUpdate{
		Status:       ProjectorStatusRateLimited,
		LastError:    "rate limited",
		LastPolledAt: errorAt,
	})
	require.NoError(t, err)
	require.Equal(t, success.ID, failed.ID)
	require.Equal(t, ProjectorStatusRateLimited, failed.Status)
	require.Equal(t, "rate limited", failed.LastError)
	require.Equal(t, 1, failed.ConsecutiveErrors)
	require.NotNil(t, failed.LastSeenAt)
	require.Equal(t, seenAt, *failed.LastSeenAt)
	require.Equal(t, `{"knownTags":["v1"]}`, failed.StateJSON)

	recoveredAt := errorAt.Add(time.Minute)
	recovered, err := store.MarkProjectorCheckpointSuccess(ctx, identity, ProjectorCheckpointUpdate{
		LastSeenAt:        &seenAt,
		LastSourceEventID: "gitea:owner/memory.mem:commit:def",
		StateJSON:         `{"knownTags":["v2"]}`,
		LastPolledAt:      recoveredAt,
	})
	require.NoError(t, err)
	require.Equal(t, ProjectorStatusOK, recovered.Status)
	require.Empty(t, recovered.LastError)
	require.Nil(t, recovered.LastErrorAt)
	require.Equal(t, 0, recovered.ConsecutiveErrors)
	require.Equal(t, `{"knownTags":["v2"]}`, recovered.StateJSON)
}
