// Copyright (C) 2026 Yota Hamada
// SPDX-License-Identifier: GPL-3.0-or-later

package filequeue_test

import (
	"testing"

	"github.com/dagucloud/dagu/internal/core/exec"
	"github.com/dagucloud/dagu/internal/persis/filequeue"
	"github.com/dagucloud/dagu/internal/test"
	"github.com/stretchr/testify/require"
)

func TestStore(t *testing.T) {
	t.Parallel()

	th := test.Setup(t)

	// Create a new store
	store := filequeue.New(th.Config.Paths.QueueDir)

	// Check if the store is empty
	length, err := store.Len(th.Context, "test-name")
	require.NoError(t, err, "expected no error when getting store length")
	require.Equal(t, 0, length, "expected store length to be 0")

	// Add a job to thestore
	err = store.Enqueue(th.Context, "test-name", exec.QueuePriorityLow, exec.DAGRunRef{
		Name: "test-name",
		ID:   "test-dag",
	})
	require.NoError(t, err, "expected no error when adding job to store")

	// Check if the store length is 1
	length, err = store.Len(th.Context, "test-name")
	require.NoError(t, err, "expected no error when getting store length")
	require.Equal(t, 1, length, "expected store length to be 1")

	// Check if other queue is empty
	length, err = store.Len(th.Context, "other-name")
	require.NoError(t, err, "expected no error when getting store length")
	require.Equal(t, 0, length, "expected store length to be 0")

	// Check if dequeue returns the job
	// Note: After dequeue, the file is removed, so we can't call Data() anymore.
	// We use the embedded ItemData fields directly instead.
	job, err := store.DequeueByName(th.Context, "test-name")
	require.NoError(t, err, "expected no error when dequeueing job from store")
	require.NotNil(t, job, "expected job to be not nil")
	require.Contains(t, job.ID(), "test-dag", "expected job ID to contain 'test-dag'")

	// Check if the queue is empty again
	length, err = store.Len(th.Context, "test-name")
	require.NoError(t, err, "expected no error when getting store length")
	require.Equal(t, 0, length, "expected store length to be 0")
}

func TestStore_DequeueByDAGRunID(t *testing.T) {
	t.Parallel()

	th := test.Setup(t)

	// Create a newstore
	store := filequeue.New(th.Config.Paths.QueueDir)

	// Add a job to thestore
	err := store.Enqueue(th.Context, "test-name", exec.QueuePriorityLow, exec.DAGRunRef{
		Name: "test-name",
		ID:   "test-dag",
	})
	require.NoError(t, err, "expected no error when adding job to store")

	// Add another job to thestore
	err = store.Enqueue(th.Context, "test-name", exec.QueuePriorityLow, exec.DAGRunRef{
		Name: "test-name",
		ID:   "test-dag-2",
	})
	require.NoError(t, err, "expected no error when adding job to store")

	// Check if dequeue by dag-run ID returns the job
	// Note: After dequeue, the file is removed, so we can't call Data() anymore.
	// We verify using the ID() method which still works.
	jobs, err := store.DequeueByDAGRunID(th.Context, "test-name", exec.DAGRunRef{
		Name: "test-name",
		ID:   "test-dag-2",
	})
	require.NoError(t, err, "expected no error when dequeueing job by dag-run ID from store")
	require.Len(t, jobs, 1, "expected to dequeue one job")
	require.Contains(t, jobs[0].ID(), "test-dag-2", "expected job ID to contain 'test-dag-2'")
}

func TestStore_DeleteByItemIDs(t *testing.T) {
	t.Parallel()

	th := test.Setup(t)
	store := filequeue.New(th.Config.Paths.QueueDir)

	require.NoError(t, store.Enqueue(th.Context, "test-name", exec.QueuePriorityLow, exec.DAGRunRef{
		Name: "test-name",
		ID:   "test-dag",
	}))
	require.NoError(t, store.Enqueue(th.Context, "test-name", exec.QueuePriorityLow, exec.DAGRunRef{
		Name: "test-name",
		ID:   "test-dag-2",
	}))

	items, err := store.List(th.Context, "test-name")
	require.NoError(t, err)
	require.Len(t, items, 2)

	deleted, err := store.DeleteByItemIDs(th.Context, "test-name", []string{items[0].ID(), "missing-item"})
	require.NoError(t, err)
	require.Equal(t, 1, deleted)

	remaining, err := store.List(th.Context, "test-name")
	require.NoError(t, err)
	require.Len(t, remaining, 1)
	require.Equal(t, items[1].ID(), remaining[0].ID())
}

func TestStore_List(t *testing.T) {
	t.Parallel()

	th := test.Setup(t)

	// Create a newstore
	store := filequeue.New(th.Config.Paths.QueueDir)

	// Add a job to thestore
	err := store.Enqueue(th.Context, "test-name", exec.QueuePriorityLow, exec.DAGRunRef{
		Name: "test-name",
		ID:   "test-dag",
	})
	require.NoError(t, err, "expected no error when adding job to store")

	// Add another job to thestore
	err = store.Enqueue(th.Context, "test-name", exec.QueuePriorityLow, exec.DAGRunRef{
		Name: "test-name",
		ID:   "test-dag-2",
	})
	require.NoError(t, err, "expected no error when adding job to store")

	// Check if list returns the jobs
	jobs, err := store.List(th.Context, "test-name")
	require.NoError(t, err, "expected no error when listing jobs from store")
	require.Len(t, jobs, 2, "expected to list two jobs")
}

func TestStore_ListCursor(t *testing.T) {
	t.Parallel()

	th := test.Setup(t)
	store := filequeue.New(th.Config.Paths.QueueDir)

	require.NoError(t, store.Enqueue(th.Context, "cursor-q", exec.QueuePriorityHigh, exec.DAGRunRef{
		Name: "cursor-q",
		ID:   "run-high",
	}))
	require.NoError(t, store.Enqueue(th.Context, "cursor-q", exec.QueuePriorityLow, exec.DAGRunRef{
		Name: "cursor-q",
		ID:   "run-low-1",
	}))
	require.NoError(t, store.Enqueue(th.Context, "cursor-q", exec.QueuePriorityLow, exec.DAGRunRef{
		Name: "cursor-q",
		ID:   "run-low-2",
	}))

	firstPage, err := store.ListCursor(th.Context, "cursor-q", "", 2)
	require.NoError(t, err)
	require.Len(t, firstPage.Items, 2)
	require.True(t, firstPage.HasMore)
	require.NotEmpty(t, firstPage.NextCursor)

	firstData, err := firstPage.Items[0].Data()
	require.NoError(t, err)
	secondData, err := firstPage.Items[1].Data()
	require.NoError(t, err)
	require.Equal(t, "run-high", firstData.ID)
	require.Equal(t, "run-low-1", secondData.ID)

	secondPage, err := store.ListCursor(th.Context, "cursor-q", firstPage.NextCursor, 2)
	require.NoError(t, err)
	require.Len(t, secondPage.Items, 1)
	require.False(t, secondPage.HasMore)
	require.Empty(t, secondPage.NextCursor)

	lastData, err := secondPage.Items[0].Data()
	require.NoError(t, err)
	require.Equal(t, "run-low-2", lastData.ID)
}

func TestStore_ListCursorRejectsInvalidCursor(t *testing.T) {
	t.Parallel()

	th := test.Setup(t)
	store := filequeue.New(th.Config.Paths.QueueDir)

	_, err := store.ListCursor(th.Context, "cursor-q", "not-a-valid-cursor", 10)
	require.ErrorIs(t, err, exec.ErrInvalidCursor)
}

func TestStore_All(t *testing.T) {
	t.Parallel()

	th := test.Setup(t)

	// Create a newstore
	store := filequeue.New(th.Config.Paths.QueueDir)

	// Add a job to thestore
	err := store.Enqueue(th.Context, "test-name", exec.QueuePriorityLow, exec.DAGRunRef{
		Name: "test-name",
		ID:   "test-dag",
	})
	require.NoError(t, err, "expected no error when adding job to store")

	// Add another job to thestore
	err = store.Enqueue(th.Context, "test-name2", exec.QueuePriorityHigh, exec.DAGRunRef{
		Name: "test-name2",
		ID:   "test-dag-2",
	})
	require.NoError(t, err, "expected no error when adding job to store")

	// Check if all returns the jobs
	jobs, err := store.All(th.Context)
	require.NoError(t, err, "expected no error when listing all jobs from store")
	require.Len(t, jobs, 2, "expected to list two jobs")

	// Check if the jobs are sorted by priority
	data1, err := jobs[0].Data()
	require.NoError(t, err, "expected no error when getting job data")
	data2, err := jobs[1].Data()
	require.NoError(t, err, "expected no error when getting job data")

	// Check if the jobs are sorted by priority
	require.Equal(t, "test-name2", data1.Name, "expected job name to be 'test-name'")
	require.Equal(t, "test-dag-2", data1.ID, "expected job ID to be 'test-dag-2'")
	require.Equal(t, "test-name", data2.Name, "expected job name to be 'test-name'")
	require.Equal(t, "test-dag", data2.ID, "expected job ID to be 'test-dag'")
}
