package app

import (
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/require"
)

func TestStorePersistsAndDeduplicatesMessage(t *testing.T) {
	t.Parallel()
	path := filepath.Join(t.TempDir(), "jobs.json")
	store, err := OpenStore(path)
	require.NoError(t, err)

	created, inserted, err := store.Enqueue(Job{MessageID: "om_1", ChatID: "oc_1", SenderOpenID: "ou_1", Request: "diagnose", Mode: JobModeAnalyze})
	require.NoError(t, err)
	require.True(t, inserted)

	duplicate, inserted, err := store.Enqueue(Job{MessageID: "om_1", Request: "different"})
	require.NoError(t, err)
	require.False(t, inserted)
	require.Equal(t, created.ID, duplicate.ID)

	reloaded, err := OpenStore(path)
	require.NoError(t, err)
	actual, ok := reloaded.Get(created.ID)
	require.True(t, ok)
	require.Equal(t, "diagnose", actual.Request)
}

func TestStoreRequeuesInterruptedJob(t *testing.T) {
	t.Parallel()
	path := filepath.Join(t.TempDir(), "jobs.json")
	store, err := OpenStore(path)
	require.NoError(t, err)
	created, _, err := store.Enqueue(Job{MessageID: "om_2", Mode: JobModeAnalyze})
	require.NoError(t, err)
	_, err = store.Update(created.ID, func(job *Job) { job.Status = JobRunning })
	require.NoError(t, err)

	reloaded, err := OpenStore(path)
	require.NoError(t, err)
	actual, ok := reloaded.Get(created.ID)
	require.True(t, ok)
	require.Equal(t, JobQueued, actual.Status)
	require.Contains(t, actual.Error, "service restart")
}

func TestStoreFindsLatestSuccessfulAnalyzeThreadInSameLarkRoot(t *testing.T) {
	t.Parallel()
	store, err := OpenStore(filepath.Join(t.TempDir(), "jobs.json"))
	require.NoError(t, err)

	first, _, err := store.Enqueue(Job{MessageID: "om_1", ChatID: "oc_1", LarkRootMessageID: "om_root", Mode: JobModeAnalyze})
	require.NoError(t, err)
	_, err = store.Update(first.ID, func(job *Job) {
		job.Status = JobSucceeded
		job.ThreadID = "thread-1"
	})
	require.NoError(t, err)
	second, _, err := store.Enqueue(Job{MessageID: "om_2", ChatID: "oc_1", LarkRootMessageID: "om_root", Mode: JobModeAnalyze})
	require.NoError(t, err)
	_, err = store.Update(second.ID, func(job *Job) {
		job.Status = JobSucceeded
		job.ThreadID = "thread-2"
	})
	require.NoError(t, err)
	failed, _, err := store.Enqueue(Job{MessageID: "om_3", ChatID: "oc_1", LarkRootMessageID: "om_root", Mode: JobModeAnalyze})
	require.NoError(t, err)
	_, err = store.Update(failed.ID, func(job *Job) {
		job.Status = JobFailed
		job.ThreadID = "thread-failed"
	})
	require.NoError(t, err)
	current, _, err := store.Enqueue(Job{MessageID: "om_4", ChatID: "oc_1", LarkRootMessageID: "om_root", Mode: JobModeAnalyze})
	require.NoError(t, err)

	previous, ok := store.LatestSuccessfulAnalyzeThread("oc_1", "om_root", current.ID)
	require.True(t, ok)
	require.Equal(t, second.ID, previous.ID)
	require.Equal(t, "thread-2", previous.ThreadID)
	_, ok = store.LatestSuccessfulAnalyzeThread("oc_other", "om_root", current.ID)
	require.False(t, ok)
	_, ok = store.LatestSuccessfulAnalyzeThread("oc_1", "om_other_root", current.ID)
	require.False(t, ok)
}

func TestStoreFindsPreDeploymentRootJobByMessageID(t *testing.T) {
	t.Parallel()
	store, err := OpenStore(filepath.Join(t.TempDir(), "jobs.json"))
	require.NoError(t, err)
	legacy, _, err := store.Enqueue(Job{
		MessageID: "om_root", ChatID: "oc_1", Mode: JobModeAnalyze,
	})
	require.NoError(t, err)
	_, err = store.Update(legacy.ID, func(job *Job) {
		job.Status = JobSucceeded
		job.ThreadID = "thread-legacy"
	})
	require.NoError(t, err)

	previous, ok := store.LatestSuccessfulAnalyzeThread("oc_1", "om_root", "current")
	require.True(t, ok)
	require.Equal(t, legacy.ID, previous.ID)
	require.Equal(t, "thread-legacy", previous.ThreadID)
}

func TestStorePersistsInitializedCandidateCursorIncludingZero(t *testing.T) {
	t.Parallel()
	path := filepath.Join(t.TempDir(), "jobs.json")
	store, err := OpenStore(path)
	require.NoError(t, err)
	value, initialized := store.CandidateCursor()
	require.Zero(t, value)
	require.False(t, initialized)

	require.NoError(t, store.SetCandidateCursor(0))
	reloaded, err := OpenStore(path)
	require.NoError(t, err)
	value, initialized = reloaded.CandidateCursor()
	require.Zero(t, value)
	require.True(t, initialized)
	require.NoError(t, reloaded.SetCandidateCursor(8))
	require.ErrorContains(t, reloaded.SetCandidateCursor(7), "cannot move backwards")
}

func TestStoreFindsOnlyPendingTerminalSingleTicketNotifications(t *testing.T) {
	t.Parallel()
	store, err := OpenStore(filepath.Join(t.TempDir(), "jobs.json"))
	require.NoError(t, err)
	ticket, _, err := store.Enqueue(Job{MessageID: "ticket", Mode: JobModeTicket})
	require.NoError(t, err)
	_, err = store.Update(ticket.ID, func(job *Job) {
		job.Status = JobSucceeded
		job.NotificationStatus = NotificationPending
	})
	require.NoError(t, err)
	analyze, _, err := store.Enqueue(Job{MessageID: "analyze", Mode: JobModeAnalyze})
	require.NoError(t, err)
	_, err = store.Update(analyze.ID, func(job *Job) {
		job.Status = JobFailed
		job.NotificationStatus = NotificationPending
	})
	require.NoError(t, err)
	require.Equal(t, []string{ticket.ID}, store.PendingNotificationIDs())
}

func TestStoreFailsInterruptedSingleTicketInsteadOfRepeatingExternalEffects(t *testing.T) {
	t.Parallel()
	path := filepath.Join(t.TempDir(), "jobs.json")
	store, err := OpenStore(path)
	require.NoError(t, err)
	job, _, err := store.Enqueue(Job{MessageID: "ticket-running", Mode: JobModeTicket})
	require.NoError(t, err)
	_, err = store.Update(job.ID, func(current *Job) { current.Status = JobRunning })
	require.NoError(t, err)

	reloaded, err := OpenStore(path)
	require.NoError(t, err)
	interrupted, ok := reloaded.Get(job.ID)
	require.True(t, ok)
	require.Equal(t, JobFailed, interrupted.Status)
	require.Equal(t, NotificationPending, interrupted.NotificationStatus)
	require.Contains(t, interrupted.Error, "human readback")
	require.Empty(t, reloaded.QueuedIDs())
}
