package server

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

	"github.com/pkg/errors"
	"github.com/stretchr/testify/require"

	"github.com/kopia/kopia/internal/clock"
	"github.com/kopia/kopia/internal/repotesting"
	"github.com/kopia/kopia/notification/notifytemplate"
	"github.com/kopia/kopia/repo"
	"github.com/kopia/kopia/repo/maintenance"
)

type testServer struct {
	mu sync.Mutex

	runCounter            atomic.Int32
	refreshSchedulerCount atomic.Int32

	log func(msg string, args ...any)

	// +checklocks:mu
	err error
}

func (s *testServer) runMaintenanceTask(ctx context.Context, dr repo.DirectRepository) error {
	s.runCounter.Add(1)

	if s.log != nil {
		s.log("runMaintenanceTask")
	}

	s.mu.Lock()
	ne := s.err
	s.err = nil
	s.mu.Unlock()

	return ne
}

func (s *testServer) refreshScheduler(reason string) {
	s.refreshSchedulerCount.Add(1)
}

func (s *testServer) enableErrorNotifications() bool {
	return false
}

func (s *testServer) notificationTemplateOptions() notifytemplate.Options {
	return notifytemplate.DefaultOptions
}

func TestServerMaintenance(t *testing.T) {
	ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant)

	require.NoError(t, repo.DirectWriteSession(ctx, env.RepositoryWriter, repo.WriteSessionOptions{}, func(ctx context.Context, dw repo.DirectRepositoryWriter) error {
		return maintenance.SetParams(ctx, dw, &maintenance.Params{
			Owner: env.Repository.ClientOptions().UsernameAtHost(),
			QuickCycle: maintenance.CycleParams{
				Enabled:  true,
				Interval: 5 * time.Second,
			},
			FullCycle: maintenance.CycleParams{
				Enabled:  true,
				Interval: 10 * time.Second,
			},
		})
	}))

	ts := &testServer{log: t.Logf}

	mm := maybeStartMaintenanceManager(ctx, env.RepositoryWriter, ts, time.Minute)
	require.Equal(t, time.Time{}, mm.nextMaintenanceNoEarlierThan)

	defer mm.stop(ctx)

	// trigger and make sure it runs
	mm.trigger()
	require.Eventually(t, func() bool {
		return ts.runCounter.Load() == 1 && ts.refreshSchedulerCount.Load() == 1
	}, 3*time.Second, 10*time.Millisecond)

	ts.mu.Lock()
	ts.err = errors.New("some error")
	ts.mu.Unlock()

	mm.trigger()

	require.Eventually(t, func() bool {
		mm.mu.Lock()
		defer mm.mu.Unlock()

		return ts.runCounter.Load() == 2 && !mm.nextMaintenanceNoEarlierThan.IsZero()
	}, 3*time.Second, 10*time.Millisecond)

	// after a failure next maintenance time should be deferred by a minute.
	require.Greater(t, mm.nextMaintenanceTime().Sub(clock.Now()), 50*time.Second)
}

func TestServerMaintenanceReadOnlyRepoConnection(t *testing.T) {
	ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant)

	require.NoError(t, repo.DirectWriteSession(ctx, env.RepositoryWriter, repo.WriteSessionOptions{}, func(ctx context.Context, dw repo.DirectRepositoryWriter) error {
		return maintenance.SetParams(ctx, dw, &maintenance.Params{
			Owner: env.Repository.ClientOptions().UsernameAtHost(),
			QuickCycle: maintenance.CycleParams{
				Enabled:  true,
				Interval: 5 * time.Second,
			},
			FullCycle: maintenance.CycleParams{
				Enabled:  true,
				Interval: 10 * time.Second,
			},
		})
	}))

	dr, ok := env.Repository.(repo.DirectRepository)
	require.True(t, ok, "not a direct repository connection")

	dr.Refresh(ctx)

	// make repo read-only
	co := env.Repository.ClientOptions()
	co.ReadOnly = true

	repo.SetClientOptions(ctx, env.ConfigFile(), co)

	env.MustReopen(t)

	ts := &testServer{log: t.Logf}
	mm := maybeStartMaintenanceManager(ctx, env.RepositoryWriter, ts, time.Minute)

	require.Nil(t, mm, "maintenance task should not be created on read-only repo")
}
