package paths

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/filecoin-project/go-state-types/abi"

	"github.com/filecoin-project/lotus/storage/sealer/storiface"
)

var aSector = abi.SectorID{
	Miner:  2,
	Number: 9000,
}

func TestCanLock(t *testing.T) {
	lk := sectorLock{
		r: [storiface.FileTypes]uint{},
		w: storiface.FTNone,
	}

	require.Equal(t, true, lk.canLock(storiface.FTUnsealed, storiface.FTNone))
	require.Equal(t, true, lk.canLock(storiface.FTNone, storiface.FTUnsealed))

	ftAll := storiface.FTUnsealed | storiface.FTSealed | storiface.FTCache

	require.Equal(t, true, lk.canLock(ftAll, storiface.FTNone))
	require.Equal(t, true, lk.canLock(storiface.FTNone, ftAll))

	lk.r[0] = 1 // unsealed read taken

	require.Equal(t, true, lk.canLock(storiface.FTUnsealed, storiface.FTNone))
	require.Equal(t, false, lk.canLock(storiface.FTNone, storiface.FTUnsealed))

	require.Equal(t, true, lk.canLock(ftAll, storiface.FTNone))
	require.Equal(t, false, lk.canLock(storiface.FTNone, ftAll))

	require.Equal(t, true, lk.canLock(storiface.FTNone, storiface.FTSealed|storiface.FTCache))
	require.Equal(t, true, lk.canLock(storiface.FTUnsealed, storiface.FTSealed|storiface.FTCache))

	lk.r[0] = 0

	lk.w = storiface.FTSealed

	require.Equal(t, true, lk.canLock(storiface.FTUnsealed, storiface.FTNone))
	require.Equal(t, true, lk.canLock(storiface.FTNone, storiface.FTUnsealed))

	require.Equal(t, false, lk.canLock(storiface.FTSealed, storiface.FTNone))
	require.Equal(t, false, lk.canLock(storiface.FTNone, storiface.FTSealed))

	require.Equal(t, false, lk.canLock(ftAll, storiface.FTNone))
	require.Equal(t, false, lk.canLock(storiface.FTNone, ftAll))
}

func TestIndexLocksSeq(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)

	ilk := &indexLocks{
		locks: map[abi.SectorID]*sectorLock{},
	}

	require.NoError(t, ilk.StorageLock(ctx, aSector, storiface.FTNone, storiface.FTUnsealed))
	cancel()

	ctx, cancel = context.WithTimeout(context.Background(), time.Second)
	require.NoError(t, ilk.StorageLock(ctx, aSector, storiface.FTNone, storiface.FTUnsealed))
	cancel()

	ctx, cancel = context.WithTimeout(context.Background(), time.Second)
	require.NoError(t, ilk.StorageLock(ctx, aSector, storiface.FTNone, storiface.FTUnsealed))
	cancel()

	ctx, cancel = context.WithTimeout(context.Background(), time.Second)
	require.NoError(t, ilk.StorageLock(ctx, aSector, storiface.FTUnsealed, storiface.FTNone))
	cancel()

	ctx, cancel = context.WithTimeout(context.Background(), time.Second)
	require.NoError(t, ilk.StorageLock(ctx, aSector, storiface.FTNone, storiface.FTUnsealed))
	cancel()

	ctx, cancel = context.WithTimeout(context.Background(), time.Second)
	require.NoError(t, ilk.StorageLock(ctx, aSector, storiface.FTNone, storiface.FTUnsealed))
	cancel()
}

func TestIndexLocksBlockOn(t *testing.T) {
	test := func(r1 storiface.SectorFileType, w1 storiface.SectorFileType, r2 storiface.SectorFileType, w2 storiface.SectorFileType) func(t *testing.T) {
		return func(t *testing.T) {
			ctx, cancel := context.WithCancel(context.Background())

			ilk := &indexLocks{
				locks: map[abi.SectorID]*sectorLock{},
			}

			require.NoError(t, ilk.StorageLock(ctx, aSector, r1, w1))

			sch := make(chan struct{})
			go func() {
				ctx, cancel := context.WithCancel(context.Background())

				sch <- struct{}{}

				require.NoError(t, ilk.StorageLock(ctx, aSector, r2, w2))
				cancel()

				sch <- struct{}{}
			}()

			<-sch

			select {
			case <-sch:
				t.Fatal("that shouldn't happen")
			case <-time.After(40 * time.Millisecond):
			}

			cancel()

			select {
			case <-sch:
			case <-time.After(time.Second):
				t.Fatal("timed out")
			}
		}
	}

	t.Run("readBlocksWrite", test(storiface.FTUnsealed, storiface.FTNone, storiface.FTNone, storiface.FTUnsealed))
	t.Run("writeBlocksRead", test(storiface.FTNone, storiface.FTUnsealed, storiface.FTUnsealed, storiface.FTNone))
	t.Run("writeBlocksWrite", test(storiface.FTNone, storiface.FTUnsealed, storiface.FTNone, storiface.FTUnsealed))
}

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

	ilk := &indexLocks{
		locks: map[abi.SectorID]*sectorLock{},
	}

	require.NoError(t, ilk.StorageLock(ctx, aSector, storiface.FTUnsealed, storiface.FTNone))

	sch := make(chan struct{})
	go func() {
		ctx, cancel := context.WithCancel(context.Background())

		sch <- struct{}{}

		require.NoError(t, ilk.StorageLock(ctx, aSector, storiface.FTNone, storiface.FTUnsealed))
		cancel()

		sch <- struct{}{}
	}()

	<-sch

	select {
	case <-sch:
		t.Fatal("that shouldn't happen")
	case <-time.After(40 * time.Millisecond):
	}

	cancel()

	select {
	case <-sch:
	case <-time.After(time.Second):
		t.Fatal("timed out")
	}
}
