package store

import (
	"context"
	"errors"
	"slices"
	"strconv"
	"strings"
	"testing"
)

// allStates — полный перечень состояний машины (для проверки хорошей
// сформированности графа). Держим локально в тесте: если добавится новое
// состояние, тест напомнит внести его сюда и в граф.
var allStates = []State{
	StateCatched, StateDownloading, StateCompleted, StateRecognizing,
	StateReview, StateLinking, StateDone, StateDeferred, StateStuck,
	StateFailed, StateCancelled, StateReverted,
	StateTargetMissing, StateOrphaned, StateDeleted,
}

// seedState заводит загрузку и приводит её к нужному состоянию кратчайшим
// доверенным путём (в обход гейта графа — через прямой UPDATE), чтобы тест
// проверял именно проверяемый переход, а не путь подготовки.
func seedState(t *testing.T, st *Store, ih string, state State) string {
	t.Helper()
	ctx := context.Background()
	d := &Download{SourceType: SourceMagnet, SourceRef: "magnet:?xt=urn:btih:" + ih, State: StateCatched}
	if _, err := st.CreateDownloadIfNoActive(ctx, d, []string{ih}, nil); err != nil {
		t.Fatalf("seed create: %v", err)
	}
	if state != StateCatched {
		if _, err := st.DB.ExecContext(ctx,
			`UPDATE download SET state = ? WHERE id = ?`, string(state), d.ID); err != nil {
			t.Fatalf("seed set state %s: %v", state, err)
		}
	}
	return d.ID
}

func stateOf(t *testing.T, st *Store, id string) State {
	t.Helper()
	d, err := st.GetDownload(context.Background(), id)
	if err != nil {
		t.Fatalf("get: %v", err)
	}
	return d.State
}

// Граф хорошо сформирован: все состояния из ключей и значений — известные, и
// ни один список не перечисляет сам ключ (петли не объявляются явно —
// самопереход добавляет invertTransitions).
func TestTransitionGraphWellFormed(t *testing.T) {
	known := func(s State) bool { return slices.Contains(allStates, s) }
	for from, tos := range allowedTransitions {
		if !known(from) {
			t.Errorf("неизвестное состояние-ключ: %q", from)
		}
		for _, to := range tos {
			if !known(to) {
				t.Errorf("%s → неизвестное состояние %q", from, to)
			}
			if to == from {
				t.Errorf("%s: петля перечислена явно (самопереход неявен)", from)
			}
		}
	}
	// Каждое состояние присутствует в графе как ключ — иначе setState fail-closed
	// отклонит переход в него.
	for _, s := range allStates {
		if _, ok := allowedTransitions[s]; !ok {
			t.Errorf("состояние %q отсутствует среди ключей графа", s)
		}
	}
}

// Инвариант Defer: deferred — легальная цель из каждого не-терминального
// состояния, КРОМЕ пре-источникового catched (Defer его отклоняет: раздачи в
// qBittorrent ещё нет, откладывать нечего — MAJOR-6) и самого deferred (это
// самопереход). Ловит класс дыры «забыли состояние» (напр. linking после краха
// процесса).
func TestDeferReachableFromEveryNonTerminalButCatched(t *testing.T) {
	for _, s := range allStates {
		if s.IsTerminal() || s == StateDeferred || s == StateCatched {
			continue // deferred → deferred покрыт самопереходом; catched исключён
		}
		if !slices.Contains(transitionSources[StateDeferred], s) {
			t.Errorf("%s → deferred не легально (Defer допускает любое не-терминальное, кроме catched)", s)
		}
	}
	// Пре-источниковое catched → deferred не легально: ребро снято из графа
	// заодно с гардом в Worker.Defer.
	if slices.Contains(transitionSources[StateDeferred], StateCatched) {
		t.Errorf("catched → deferred легально, но должно быть снято (Defer отклоняет catched)")
	}
}

// Инвариант универсального стоп-крана Dismiss: cancelled — легальная цель из
// ЛЮБОГО состояния, кроме deleted (строго терминален) и самого cancelled
// (самопереход). Шире инварианта Defer: покрывает и терминальные
// done/failed/reverted/target_missing/orphaned. НЕ объединять с проверкой
// deferred — у них разные множества источников.
func TestCancelledReachableFromEveryStateButDeleted(t *testing.T) {
	for _, s := range allStates {
		if s == StateDeleted || s == StateCancelled {
			continue // deleted строго терминален; cancelled → cancelled — самопереход
		}
		if !slices.Contains(transitionSources[StateCancelled], s) {
			t.Errorf("%s → cancelled не легально (Dismiss/Cancel допускают любое состояние, кроме deleted)", s)
		}
	}
}

// Объявленные не-revive рёбра проходят через SetDownloadState.
func TestSetStateAllowsDeclaredEdges(t *testing.T) {
	edges := []struct{ from, to State }{
		{StateDownloading, StateCompleted},
		{StateDownloading, StateStuck},
		{StateCompleted, StateRecognizing},
		{StateRecognizing, StateReview},
		{StateRecognizing, StateLinking},
		{StateReview, StateLinking},
		{StateReview, StateRecognizing},
		{StateLinking, StateDone},
		{StateLinking, StateReview},
		{StateDone, StateReverted},
		{StateStuck, StateCancelled},
		{StateReview, StateDeferred},
		// Стоп-кран Dismiss: терминал → cancelled идёт обычным SetDownloadState
		// (цель cancelled терминальна → гард терминальности не мешает, revive не
		// нужен).
		{StateDone, StateCancelled},
		{StateFailed, StateCancelled},
		{StateReverted, StateCancelled},
		{StateTargetMissing, StateCancelled},
		{StateOrphaned, StateCancelled},
	}
	for i, e := range edges {
		st := newTestStore(t)
		ih := infohashN(i)
		id := seedState(t, st, ih, e.from)
		if err := st.SetDownloadState(context.Background(), id, e.to, "", ""); err != nil {
			t.Errorf("легальное ребро %s → %s отклонено: %v", e.from, e.to, err)
			continue
		}
		if got := stateOf(t, st, id); got != e.to {
			t.Errorf("%s → %s: состояние стало %q", e.from, e.to, got)
		}
	}
}

// Preflight-рёбра reconcileToReality при пропавшем источнике: из ревью/
// терминальных состояний в orphaned/deleted проходят через SetDownloadState
// (цель терминальна → гард терминальности не мешает).
func TestPreflightDesyncEdges(t *testing.T) {
	edges := []struct{ from, to State }{
		{StateReview, StateOrphaned},
		{StateReview, StateDeleted},
		{StateDeferred, StateOrphaned},
		{StateDeferred, StateDeleted},
		{StateReverted, StateOrphaned},
		{StateReverted, StateDeleted},
		{StateCancelled, StateOrphaned},
		{StateCancelled, StateDeleted},
	}
	for i, e := range edges {
		st := newTestStore(t)
		id := seedState(t, st, infohashN(i), e.from)
		if err := st.SetDownloadState(context.Background(), id, e.to, "", ""); err != nil {
			t.Errorf("preflight-ребро %s → %s отклонено: %v", e.from, e.to, err)
			continue
		}
		if got := stateOf(t, st, id); got != e.to {
			t.Errorf("%s → %s: состояние стало %q", e.from, e.to, got)
		}
	}
}

// Необъявленные рёбра отклоняются, состояние не меняется.
func TestSetStateRejectsUndeclaredEdges(t *testing.T) {
	edges := []struct{ from, to State }{
		{StateReview, StateDone},
		{StateDownloading, StateDone},
		{StateCompleted, StateLinking},
		{StateDownloading, StateReview},
	}
	for i, e := range edges {
		st := newTestStore(t)
		id := seedState(t, st, infohashN(i), e.from)
		err := st.SetDownloadState(context.Background(), id, e.to, "", "")
		if err == nil {
			t.Errorf("нелегальное ребро %s → %s прошло", e.from, e.to)
			continue
		}
		if !strings.Contains(err.Error(), "illegal transition") {
			t.Errorf("%s → %s: ожидалось 'illegal transition', got: %v", e.from, e.to, err)
		}
		if got := stateOf(t, st, id); got != e.from {
			t.Errorf("%s → %s отклонён, но состояние стало %q", e.from, e.to, got)
		}
	}
}

// Самопереход (идемпотентная переустановка того же состояния) разрешён.
func TestSelfTransitionAllowed(t *testing.T) {
	st := newTestStore(t)
	id := seedState(t, st, infohashN(0), StateDeferred)
	if err := st.SetDownloadState(context.Background(), id, StateDeferred, "", ""); err != nil {
		t.Fatalf("самопереход deferred → deferred отклонён: %v", err)
	}
	if got := stateOf(t, st, id); got != StateDeferred {
		t.Errorf("состояние стало %q", got)
	}
}

// Ребро из терминального состояния в активное проходит только revive-путём
// (ActivateIfNoOtherActive), но не обычным SetDownloadState.
func TestTerminalReviveOnlyViaActivate(t *testing.T) {
	ctx := context.Background()

	// SetDownloadState (не-revive): failed → downloading отклоняется гардом
	// терминальности, ошибка указывает на revive.
	st := newTestStore(t)
	id := seedState(t, st, infohashN(0), StateFailed)
	err := st.SetDownloadState(ctx, id, StateDownloading, "", "")
	if err == nil {
		t.Fatal("failed → downloading через SetDownloadState прошло")
	}
	if !strings.Contains(err.Error(), "terminal revive") {
		t.Errorf("ожидалось 'terminal revive', got: %v", err)
	}
	if got := stateOf(t, st, id); got != StateFailed {
		t.Errorf("состояние изменилось на %q", got)
	}

	// ActivateIfNoOtherActive (revive): тот же переход при свободном infohash
	// проходит.
	st2 := newTestStore(t)
	id2 := seedState(t, st2, infohashN(1), StateFailed)
	if err := st2.ActivateIfNoOtherActive(ctx, id2, StateDownloading, "", ""); err != nil {
		t.Fatalf("revive failed → downloading отклонён: %v", err)
	}
	if got := stateOf(t, st2, id2); got != StateDownloading {
		t.Errorf("revive: состояние стало %q, want downloading", got)
	}
}

// Отсутствующая загрузка → ErrNotFound (гейт графа не маскирует «не найдено»).
func TestSetStateNotFound(t *testing.T) {
	st := newTestStore(t)
	err := st.SetDownloadState(context.Background(), "01hnonexistentnonexistent", StateCompleted, "", "")
	if !errors.Is(err, ErrNotFound) {
		t.Errorf("ожидался ErrNotFound, got: %v", err)
	}
}

// infohashN — детерминированный 40-hex инфохэш по индексу (уникальность между
// подтестами без общего состояния). Цифры и 'a'-паддинг — валидный hex.
func infohashN(n int) string {
	s := strconv.Itoa(n)
	return strings.Repeat("a", 40-len(s)) + s
}
