package timer_test

import (
	"errors"
	"testing"

	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/timer"
	"github.com/stretchr/testify/require"
)

func tickN(t *timer.BlockTimer, n uint32) {
	for range n {
		t.Tick(0)
	}
}

// This test emulates inner ring handling of a new epoch and a new block.
// "resetting" consists of ticking the current height as well and invoking `Reset`.
func TestIRBlockTimer_Reset(t *testing.T) {
	var baseCounter [2]int
	const blockDur = uint32(3)

	bt1 := timer.NewBlockTimer(
		func() (uint32, error) { return blockDur, nil },
		func() { baseCounter[0]++ })
	bt2 := timer.NewBlockTimer(
		func() (uint32, error) { return blockDur, nil },
		func() { baseCounter[1]++ })

	require.NoError(t, bt1.Reset())
	require.NoError(t, bt2.Reset())

	run := func(bt *timer.BlockTimer, direct bool) {
		if direct {
			bt.Tick(1)
			require.NoError(t, bt.Reset())
			bt.Tick(1)
		} else {
			bt.Tick(1)
			bt.Tick(1)
			require.NoError(t, bt.Reset())
		}
		bt.Tick(2)
		bt.Tick(3)
	}

	run(bt1, true)
	run(bt2, false)
	require.Equal(t, baseCounter[0], baseCounter[1])
}

func TestBlockTimer_ResetChangeDuration(t *testing.T) {
	var dur uint32 = 2
	var err error
	var counter int

	bt := timer.NewBlockTimer(
		func() (uint32, error) { return dur, err },
		func() { counter++ })

	require.NoError(t, bt.Reset())

	tickN(bt, 2)
	require.Equal(t, 1, counter)

	t.Run("return error", func(t *testing.T) {
		dur = 5
		err = errors.New("my awesome error")
		require.ErrorIs(t, bt.Reset(), err)

		tickN(bt, 2)
		require.Equal(t, 2, counter)
	})
	t.Run("change duration", func(t *testing.T) {
		dur = 5
		err = nil
		require.NoError(t, bt.Reset())

		tickN(bt, 5)
		require.Equal(t, 3, counter)
	})
}

func TestBlockTimer(t *testing.T) {
	const blockDur = uint32(10)
	baseCallCounter := uint32(0)

	bt := timer.NewBlockTimer(timer.StaticBlockMeter(blockDur), func() {
		baseCallCounter++
	})

	require.NoError(t, bt.Reset())

	intervalNum := uint32(7)

	tickN(bt, intervalNum*blockDur)

	require.Equal(t, intervalNum, uint32(baseCallCounter))
}

func TestNewOneTickTimer(t *testing.T) {
	blockDur := uint32(1)
	baseCallCounter := 0

	bt := timer.NewOneTickTimer(timer.StaticBlockMeter(blockDur), func() {
		baseCallCounter++
	})
	require.NoError(t, bt.Reset())

	tickN(bt, 10)
	require.Equal(t, 1, baseCallCounter) // happens once no matter what

	t.Run("zero duration", func(t *testing.T) {
		blockDur = uint32(0)
		baseCallCounter = 0

		bt = timer.NewOneTickTimer(timer.StaticBlockMeter(blockDur), func() {
			baseCallCounter++
		})
		require.NoError(t, bt.Reset())

		tickN(bt, 10)
		require.Equal(t, 1, baseCallCounter)
	})
}

func TestBlockTimer_TickSameHeight(t *testing.T) {
	var baseCounter int

	blockDur := uint32(2)
	bt := timer.NewBlockTimer(
		func() (uint32, error) { return blockDur, nil },
		func() { baseCounter++ })
	require.NoError(t, bt.Reset())

	check := func(t *testing.T, h uint32, base int) {
		for range 2 * int(blockDur) {
			bt.Tick(h)
			require.Equal(t, base, baseCounter)
		}
	}

	check(t, 1, 0)
	check(t, 2, 1)
	check(t, 3, 1)
	check(t, 4, 2)

	t.Run("works the same way after `Reset()`", func(t *testing.T) {
		t.Run("same block duration", func(t *testing.T) {
			require.NoError(t, bt.Reset())
			baseCounter = 0

			check(t, 1, 0)
			check(t, 2, 1)
			check(t, 3, 1)
			check(t, 4, 2)
		})
		t.Run("different block duration", func(t *testing.T) {
			blockDur = 3

			require.NoError(t, bt.Reset())
			baseCounter = 0

			check(t, 1, 0)
			check(t, 2, 0)
			check(t, 3, 1)
			check(t, 4, 1)
			check(t, 5, 1)
			check(t, 6, 2)
		})
	})
}