package daemon

import (
	"context"
	"errors"
	"fmt"
	"sync/atomic"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

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

func TestResolveReviewDiffBatchFallbackStartsPathsConcurrently(t *testing.T) {
	started := make(chan string, 2)
	release := make(chan struct{})
	loading := make(chan []AgentReviewDiffBatchResult, 1)

	go func() {
		loading <- resolveReviewDiffBatchFallback(
			context.Background(),
			[]string{" first.go ", "second.go"},
			2,
			func(_ context.Context, path string) (agent.AgentReviewDiff, error) {
				started <- path
				<-release
				return agent.AgentReviewDiff{Status: "ready", Path: path}, nil
			},
		)
	}()

	first := requireReviewFallbackValue(t, started)
	second := requireReviewFallbackValue(t, started)
	require.ElementsMatch(t, []string{" first.go ", "second.go"}, []string{first, second})

	close(release)
	outcomes := requireReviewFallbackValue(t, loading)
	require.Equal(t, " first.go ", outcomes[0].Diff.Path)
	require.Equal(t, "second.go", outcomes[1].Diff.Path)
}

func TestResolveReviewDiffBatchFallbackCapsConcurrency(t *testing.T) {
	const (
		pathCount   = 24
		concurrency = 8
	)
	paths := make([]string, pathCount)
	for index := range paths {
		paths[index] = fmt.Sprintf("file-%02d.go", index)
	}

	started := make(chan struct{}, pathCount)
	release := make(chan struct{})
	loading := make(chan []AgentReviewDiffBatchResult, 1)
	var active atomic.Int64
	var peak atomic.Int64

	go func() {
		loading <- resolveReviewDiffBatchFallback(
			context.Background(),
			paths,
			concurrency,
			func(_ context.Context, path string) (agent.AgentReviewDiff, error) {
				current := active.Add(1)
				for {
					previous := peak.Load()
					if current <= previous || peak.CompareAndSwap(previous, current) {
						break
					}
				}
				started <- struct{}{}
				<-release
				active.Add(-1)
				return agent.AgentReviewDiff{Status: "ready", Path: path}, nil
			},
		)
	}()

	for range concurrency {
		requireReviewFallbackValue(t, started)
	}
	select {
	case <-started:
		t.Fatal("fallback started more than eight paths before a worker became available")
	default:
	}

	close(release)
	outcomes := requireReviewFallbackValue(t, loading)
	require.Len(t, outcomes, pathCount)
	require.Equal(t, int64(concurrency), peak.Load())
	for index, outcome := range outcomes {
		require.NoError(t, outcome.Error)
		require.Equal(t, paths[index], outcome.Diff.Path)
	}
}

func TestResolveReviewDiffBatchFallbackIsolatesPerPathErrors(t *testing.T) {
	pathErr := errors.New("diff failed")
	paths := []string{" first.go ", "broken.go", "third.go"}

	outcomes := resolveReviewDiffBatchFallback(
		context.Background(),
		paths,
		3,
		func(_ context.Context, path string) (agent.AgentReviewDiff, error) {
			if path == "broken.go" {
				return agent.AgentReviewDiff{}, pathErr
			}
			return agent.AgentReviewDiff{Status: "ready", Path: path}, nil
		},
	)

	require.Len(t, outcomes, len(paths))
	require.Equal(t, " first.go ", outcomes[0].Diff.Path)
	require.NoError(t, outcomes[0].Error)
	require.ErrorIs(t, outcomes[1].Error, pathErr)
	require.Empty(t, outcomes[1].Diff)
	require.Equal(t, "third.go", outcomes[2].Diff.Path)
	require.NoError(t, outcomes[2].Error)
}

func TestReviewDiffBatchFallbackResultsPreserveLastDuplicate(t *testing.T) {
	paths := []string{"same.go", "other.go", "same.go"}
	outcomes := []AgentReviewDiffBatchResult{
		{Diff: agent.AgentReviewDiff{Status: "ready", Path: "same.go", Message: "first"}},
		{Diff: agent.AgentReviewDiff{Status: "ready", Path: "other.go"}},
		{Diff: agent.AgentReviewDiff{Status: "ready", Path: "same.go", Message: "last"}},
	}

	results := reviewDiffBatchFallbackResults(paths, outcomes)

	require.Len(t, results, 2)
	require.Equal(t, "last", results["same.go"].Diff.Message)
	require.Equal(t, "other.go", results["other.go"].Diff.Path)
}

func TestResolveReviewDiffBatchFallbackStopsDispatchAfterCancel(t *testing.T) {
	paths := make([]string, 32)
	for index := range paths {
		paths[index] = fmt.Sprintf("file-%02d.go", index)
	}

	ctx, cancel := context.WithCancel(context.Background())
	startedSignal := make(chan struct{}, 2)
	loading := make(chan []AgentReviewDiffBatchResult, 1)
	var started atomic.Int64

	go func() {
		loading <- resolveReviewDiffBatchFallback(
			ctx,
			paths,
			2,
			func(ctx context.Context, _ string) (agent.AgentReviewDiff, error) {
				started.Add(1)
				startedSignal <- struct{}{}
				<-ctx.Done()
				return agent.AgentReviewDiff{}, ctx.Err()
			},
		)
	}()

	requireReviewFallbackValue(t, startedSignal)
	requireReviewFallbackValue(t, startedSignal)
	cancel()

	outcomes := requireReviewFallbackValue(t, loading)
	require.Equal(t, int64(2), started.Load(), "cancellation must not dispatch the remaining paths")
	require.Len(t, outcomes, len(paths))
	for _, outcome := range outcomes {
		require.ErrorIs(t, outcome.Error, context.Canceled)
	}
}

func TestResolveReviewDiffBatchFallbackJobSkipsLoaderAfterCancel(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	var loaderCalls atomic.Int64
	outcomes := make([]AgentReviewDiffBatchResult, 1)
	resolveReviewDiffBatchFallbackJob(
		ctx,
		reviewDiffBatchFallbackJob{index: 0, path: "not-started.go"},
		outcomes,
		func(context.Context, string) (agent.AgentReviewDiff, error) {
			loaderCalls.Add(1)
			return agent.AgentReviewDiff{Status: "ready"}, nil
		},
	)

	require.Zero(t, loaderCalls.Load(), "a job received after cancellation must not enter the loader")
	require.ErrorIs(t, outcomes[0].Error, context.Canceled)
}

func requireReviewFallbackValue[T any](t *testing.T, ch <-chan T) T {
	t.Helper()
	select {
	case value := <-ch:
		return value
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for review fallback value")
		var zero T
		return zero
	}
}
