package dashboard_test

import (
	"context"
	"runtime"
	"sync"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/xerrors"

	"cdr.dev/slog/sloggers/slogtest"
	"github.com/coder/coder/v2/coderd/coderdtest"
	"github.com/coder/coder/v2/scaletest/dashboard"
	"github.com/coder/coder/v2/testutil"
)

func Test_Run(t *testing.T) {
	t.Parallel()
	if testutil.RaceEnabled() {
		t.Skip("skipping timing-sensitive test because of race detector")
	}
	if runtime.GOOS == "windows" {
		t.Skip("skipping test on Windows")
	}

	client := coderdtest.New(t, nil)
	_ = coderdtest.CreateFirstUser(t, client)

	successfulAction := func(context.Context, *dashboard.Params) error {
		return nil
	}
	failingAction := func(context.Context, *dashboard.Params) error {
		return xerrors.Errorf("failed")
	}
	hangingAction := func(ctx context.Context, _ *dashboard.Params) error {
		<-ctx.Done()
		return ctx.Err()
	}

	testActions := []dashboard.RollTableEntry{
		{0, successfulAction, "succeeds"},
		{1, failingAction, "fails"},
		{2, hangingAction, "hangs"},
	}

	log := slogtest.Make(t, &slogtest.Options{
		IgnoreErrors: true,
	})
	m := &testMetrics{}
	cfg := dashboard.Config{
		MinWait:   time.Millisecond,
		MaxWait:   10 * time.Millisecond,
		Logger:    log,
		RollTable: testActions,
	}
	r := dashboard.NewRunner(client, m, cfg)
	ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
	t.Cleanup(cancel)
	done := make(chan error)
	go func() {
		defer close(done)
		done <- r.Run(ctx, "", nil)
	}()
	err, ok := <-done
	assert.True(t, ok)
	require.NoError(t, err)

	if assert.NotEmpty(t, m.ObservedDurations["succeeds"]) {
		assert.NotZero(t, m.ObservedDurations["succeeds"][0])
	}

	if assert.NotEmpty(t, m.ObservedDurations["fails"]) {
		assert.NotZero(t, m.ObservedDurations["fails"][0])
	}

	if assert.NotEmpty(t, m.ObservedDurations["hangs"]) {
		assert.GreaterOrEqual(t, m.ObservedDurations["hangs"][0], cfg.MaxWait.Seconds())
	}
	assert.Zero(t, m.Errors["succeeds"])
	assert.NotZero(t, m.Errors["fails"])
	assert.NotZero(t, m.Errors["hangs"])
	assert.NotEmpty(t, m.Statuses["succeeds"])
	assert.NotEmpty(t, m.Statuses["fails"])
	assert.NotEmpty(t, m.Statuses["hangs"])
}

type testMetrics struct {
	sync.RWMutex
	ObservedDurations map[string][]float64
	Errors            map[string]int
	Statuses          map[string]map[string]int
}

func (m *testMetrics) ObserveDuration(action string, d time.Duration) {
	m.Lock()
	defer m.Unlock()
	if m.ObservedDurations == nil {
		m.ObservedDurations = make(map[string][]float64)
	}
	m.ObservedDurations[action] = append(m.ObservedDurations[action], d.Seconds())
}

func (m *testMetrics) IncErrors(action string) {
	m.Lock()
	defer m.Unlock()
	if m.Errors == nil {
		m.Errors = make(map[string]int)
	}
	m.Errors[action]++
}

func (m *testMetrics) IncStatuses(action string, code string) {
	m.Lock()
	defer m.Unlock()
	if m.Statuses == nil {
		m.Statuses = make(map[string]map[string]int)
	}
	if m.Statuses[action] == nil {
		m.Statuses[action] = make(map[string]int)
	}
	m.Statuses[action][code]++
}
