package list

import (
	"context"
	"errors"
	"fmt"
	"testing"

	"github.com/rclone/rclone/fs"
	"github.com/rclone/rclone/fstest/mockobject"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// Mock callback to collect the entries
func mockCallback(entries fs.DirEntries) error {
	// Do nothing or log for testing purposes
	return nil
}

func TestNewListRHelper(t *testing.T) {
	callback := mockCallback
	helper := NewHelper(callback)

	assert.NotNil(t, helper)
	assert.Equal(t, fmt.Sprintf("%p", callback), fmt.Sprintf("%p", helper.callback))
	assert.Empty(t, helper.entries)
}

func TestListRHelperAdd(t *testing.T) {
	callbackInvoked := false
	callback := func(entries fs.DirEntries) error {
		callbackInvoked = true
		return nil
	}

	helper := NewHelper(callback)
	entry := mockobject.Object("A")
	require.NoError(t, helper.Add(entry))

	assert.Len(t, helper.entries, 1)
	assert.False(t, callbackInvoked, "Callback should not be invoked before reaching 100 entries")

	// Check adding a nil entry doesn't change anything
	require.NoError(t, helper.Add(nil))
	assert.Len(t, helper.entries, 1)
	assert.False(t, callbackInvoked, "Callback should not be invoked before reaching 100 entries")
}

func TestListRHelperSend(t *testing.T) {
	entry := mockobject.Object("A")
	callbackInvoked := false
	callback := func(entries fs.DirEntries) error {
		callbackInvoked = true
		assert.Equal(t, 100, len(entries))
		for _, obj := range entries {
			assert.Equal(t, entry, obj)
		}
		return nil
	}

	helper := NewHelper(callback)

	// Add 100 entries to force the callback to be invoked
	for i := 0; i < 100; i++ {
		require.NoError(t, helper.Add(entry))
	}

	assert.Len(t, helper.entries, 0)
	assert.True(t, callbackInvoked, "Callback should be invoked after 100 entries")
}

func TestListRHelperFlush(t *testing.T) {
	entry := mockobject.Object("A")
	callbackInvoked := false
	callback := func(entries fs.DirEntries) error {
		callbackInvoked = true
		assert.Equal(t, 1, len(entries))
		for _, obj := range entries {
			assert.Equal(t, entry, obj)
		}
		return nil
	}

	helper := NewHelper(callback)
	require.NoError(t, helper.Add(entry))
	assert.False(t, callbackInvoked, "Callback should not have been invoked yet")
	require.NoError(t, helper.Flush())

	assert.True(t, callbackInvoked, "Callback should be invoked on flush")
	assert.Len(t, helper.entries, 0, "Entries should be cleared after flush")
}

type mockListPfs struct {
	t          *testing.T
	entries    fs.DirEntries
	err        error
	errorAfter int
}

func (f *mockListPfs) ListP(ctx context.Context, dir string, callback fs.ListRCallback) (err error) {
	assert.Equal(f.t, "dir", dir)
	count := 0
	for entries := f.entries; len(entries) > 0; entries = entries[2:] {
		err = callback(entries[:2])
		if err != nil {
			return err
		}
		count += 2
		if f.err != nil && count >= f.errorAfter {
			return f.err
		}
	}
	return nil
}

// check interface
var _ fs.ListPer = (*mockListPfs)(nil)

func TestListWithListP(t *testing.T) {
	ctx := context.Background()
	var entries fs.DirEntries
	for i := 0; i < 26; i++ {
		entries = append(entries, mockobject.New(fmt.Sprintf("%c", 'A'+i)))
	}
	t.Run("NoError", func(t *testing.T) {
		f := &mockListPfs{
			t:       t,
			entries: entries,
		}
		gotEntries, err := WithListP(ctx, "dir", f)
		require.NoError(t, err)
		assert.Equal(t, entries, gotEntries)
	})
	t.Run("Error", func(t *testing.T) {
		f := &mockListPfs{t: t,
			entries:    entries,
			err:        errors.New("BOOM"),
			errorAfter: 10,
		}
		gotEntries, err := WithListP(ctx, "dir", f)
		assert.Equal(t, f.err, err)
		assert.Equal(t, entries[:10], gotEntries)
	})
}
