package api

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

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
)

type fakeWorkspaceResolverCacheStore struct {
	mu          sync.Mutex
	summary     workspace.Summary
	err         error
	calls       int
	workspaceID string
	userID      string
}

func (store *fakeWorkspaceResolverCacheStore) GetWorkspaceForUser(_ context.Context, userID string, workspaceID string) (workspace.Summary, error) {
	store.mu.Lock()
	defer store.mu.Unlock()

	store.calls++
	store.userID = userID
	store.workspaceID = workspaceID
	if store.err != nil {
		return workspace.Summary{}, store.err
	}
	return store.summary, nil
}

func (store *fakeWorkspaceResolverCacheStore) callCount() int {
	store.mu.Lock()
	defer store.mu.Unlock()
	return store.calls
}

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

	store := &fakeWorkspaceResolverCacheStore{
		summary: workspace.Summary{
			WorkspaceID: "wsp_1",
			MemberID:    "mem_1",
		},
	}
	resolver := NewCachedWorkspaceResolver(store, time.Minute)

	first, err := resolver.GetWorkspaceForUser(context.Background(), " usr_1 ", " wsp_1 ")
	require.NoError(t, err)
	second, err := resolver.GetWorkspaceForUser(context.Background(), "usr_1", "wsp_1")
	require.NoError(t, err)

	require.Equal(t, first, second)
	require.Equal(t, 1, store.callCount())
	require.Equal(t, "usr_1", store.userID)
	require.Equal(t, "wsp_1", store.workspaceID)
}

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

	boom := errors.New("boom")
	store := &fakeWorkspaceResolverCacheStore{err: boom}
	resolver := NewCachedWorkspaceResolver(store, time.Minute)

	_, err := resolver.GetWorkspaceForUser(context.Background(), "usr_1", "wsp_1")
	require.ErrorIs(t, err, boom)

	store.err = nil
	store.summary = workspace.Summary{WorkspaceID: "wsp_1", MemberID: "mem_1"}
	summary, err := resolver.GetWorkspaceForUser(context.Background(), "usr_1", "wsp_1")
	require.NoError(t, err)

	require.Equal(t, "mem_1", summary.MemberID)
	require.Equal(t, 2, store.callCount())
}

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

	now := time.Date(2026, 6, 9, 12, 0, 0, 0, time.UTC)
	store := &fakeWorkspaceResolverCacheStore{
		summary: workspace.Summary{WorkspaceID: "wsp_1", MemberID: "mem_old"},
	}
	resolver := newCachedWorkspaceResolver(store, time.Second, func() time.Time {
		return now
	})

	first, err := resolver.GetWorkspaceForUser(context.Background(), "usr_1", "wsp_1")
	require.NoError(t, err)

	store.summary = workspace.Summary{WorkspaceID: "wsp_1", MemberID: "mem_new"}
	now = now.Add(500 * time.Millisecond)
	second, err := resolver.GetWorkspaceForUser(context.Background(), "usr_1", "wsp_1")
	require.NoError(t, err)

	now = now.Add(time.Second)
	third, err := resolver.GetWorkspaceForUser(context.Background(), "usr_1", "wsp_1")
	require.NoError(t, err)

	require.Equal(t, "mem_old", first.MemberID)
	require.Equal(t, "mem_old", second.MemberID)
	require.Equal(t, "mem_new", third.MemberID)
	require.Equal(t, 2, store.callCount())
}

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

	store := newBlockingWorkspaceResolverCacheStore(workspace.Summary{WorkspaceID: "wsp_1", MemberID: "mem_1"})
	resolver := NewCachedWorkspaceResolver(store, time.Minute)

	var wg sync.WaitGroup
	var resultsMu sync.Mutex
	errs := make([]error, 0)
	summaries := make([]workspace.Summary, 0)
	callResolver := func() {
		defer wg.Done()
		summary, err := resolver.GetWorkspaceForUser(context.Background(), "usr_1", "wsp_1")
		resultsMu.Lock()
		defer resultsMu.Unlock()
		errs = append(errs, err)
		summaries = append(summaries, summary)
	}

	wg.Add(1)
	go callResolver()
	require.Eventually(t, store.started, time.Second, time.Millisecond)

	for range 10 {
		wg.Add(1)
		go callResolver()
	}
	close(store.release)
	wg.Wait()

	require.Equal(t, 1, store.callCount())
	require.Len(t, summaries, 11)
	for _, err := range errs {
		require.NoError(t, err)
	}
	for _, summary := range summaries {
		require.Equal(t, "mem_1", summary.MemberID)
	}
}

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

	store := &fakeWorkspaceResolverCacheStore{
		summary: workspace.Summary{WorkspaceID: "wsp_1", MemberID: "mem_old"},
	}
	resolver := NewCachedWorkspaceResolver(store, time.Minute)

	first, err := resolver.GetWorkspaceForUser(context.Background(), "usr_1", "wsp_1")
	require.NoError(t, err)

	store.summary = workspace.Summary{WorkspaceID: "wsp_1", MemberID: "mem_new"}
	resolver.Invalidate(" usr_1 ", " wsp_1 ")
	second, err := resolver.GetWorkspaceForUser(context.Background(), "usr_1", "wsp_1")
	require.NoError(t, err)

	require.Equal(t, "mem_old", first.MemberID)
	require.Equal(t, "mem_new", second.MemberID)
	require.Equal(t, 2, store.callCount())
}

type blockingWorkspaceResolverCacheStore struct {
	mu      sync.Mutex
	summary workspace.Summary
	calls   int

	release     chan struct{}
	started     func() bool
	markStarted func()
}

func newBlockingWorkspaceResolverCacheStore(summary workspace.Summary) *blockingWorkspaceResolverCacheStore {
	started := make(chan struct{})
	store := &blockingWorkspaceResolverCacheStore{
		summary: summary,
		release: make(chan struct{}),
	}
	store.started = func() bool {
		select {
		case <-started:
			return true
		default:
			return false
		}
	}
	store.markStarted = func() {
		select {
		case <-started:
		default:
			close(started)
		}
	}
	return store
}

func (store *blockingWorkspaceResolverCacheStore) GetWorkspaceForUser(_ context.Context, _ string, _ string) (workspace.Summary, error) {
	store.mu.Lock()
	store.calls++
	store.markStarted()
	store.mu.Unlock()

	<-store.release
	return store.summary, nil
}

func (store *blockingWorkspaceResolverCacheStore) callCount() int {
	store.mu.Lock()
	defer store.mu.Unlock()
	return store.calls
}

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

	store := &fakeWorkspaceResolverCacheStore{
		summary: workspace.Summary{WorkspaceID: "wsp_1", MemberID: "mem_old"},
	}
	resolver := NewCachedWorkspaceResolver(store, time.Minute)

	_, err := resolver.GetWorkspaceForUser(context.Background(), "usr_1", "wsp_1")
	require.NoError(t, err)
	_, err = resolver.GetWorkspaceForUser(context.Background(), "usr_2", "wsp_1")
	require.NoError(t, err)

	store.summary = workspace.Summary{WorkspaceID: "wsp_1", MemberID: "mem_new"}
	resolver.InvalidateWorkspace(" wsp_1 ")
	_, err = resolver.GetWorkspaceForUser(context.Background(), "usr_1", "wsp_1")
	require.NoError(t, err)
	_, err = resolver.GetWorkspaceForUser(context.Background(), "usr_2", "wsp_1")
	require.NoError(t, err)

	require.Equal(t, 4, store.callCount())
}

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

	store := &fakeWorkspaceResolverCacheStore{
		summary: workspace.Summary{MemberID: "mem_old"},
	}
	resolver := NewCachedWorkspaceResolver(store, time.Minute)

	_, err := resolver.GetWorkspaceForUser(context.Background(), "usr_1", "wsp_1")
	require.NoError(t, err)
	_, err = resolver.GetWorkspaceForUser(context.Background(), "usr_1", "wsp_2")
	require.NoError(t, err)
	_, err = resolver.GetWorkspaceForUser(context.Background(), "usr_2", "wsp_1")
	require.NoError(t, err)

	store.summary = workspace.Summary{MemberID: "mem_new"}
	resolver.InvalidateUser(" usr_1 ")
	_, err = resolver.GetWorkspaceForUser(context.Background(), "usr_1", "wsp_1")
	require.NoError(t, err)
	_, err = resolver.GetWorkspaceForUser(context.Background(), "usr_1", "wsp_2")
	require.NoError(t, err)
	_, err = resolver.GetWorkspaceForUser(context.Background(), "usr_2", "wsp_1")
	require.NoError(t, err)

	require.Equal(t, 5, store.callCount())
}

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

	store := &fakeWorkspaceResolverCacheStore{
		summary: workspace.Summary{OrganizationID: "org_1", MemberID: "mem_old"},
	}
	resolver := NewCachedWorkspaceResolver(store, time.Minute)

	_, err := resolver.GetWorkspaceForUser(context.Background(), "usr_1", "wsp_1")
	require.NoError(t, err)
	_, err = resolver.GetWorkspaceForUser(context.Background(), "usr_2", "wsp_2")
	require.NoError(t, err)

	store.summary = workspace.Summary{OrganizationID: "org_2", MemberID: "mem_keep"}
	_, err = resolver.GetWorkspaceForUser(context.Background(), "usr_3", "wsp_3")
	require.NoError(t, err)

	store.summary = workspace.Summary{OrganizationID: "org_1", MemberID: "mem_new"}
	resolver.InvalidateOrganization(" org_1 ")
	_, err = resolver.GetWorkspaceForUser(context.Background(), "usr_1", "wsp_1")
	require.NoError(t, err)
	_, err = resolver.GetWorkspaceForUser(context.Background(), "usr_2", "wsp_2")
	require.NoError(t, err)
	_, err = resolver.GetWorkspaceForUser(context.Background(), "usr_3", "wsp_3")
	require.NoError(t, err)

	require.Equal(t, 5, store.callCount())
}
