package service

import (
	"context"
	"errors"
	"testing"

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

	"git.vakhrushev.me/av/transcriber/internal/contract"
	"git.vakhrushev.me/av/transcriber/internal/entity"
)

// killedConverter ведёт себя как настоящий `ffmpeg`, убитый по контексту:
// дожидается отмены и отдаёт отказ, который **не** несёт `context.Canceled`.
// Это не упрощение, а суть проверки: `exec` отдаёт `*exec.ExitError` с текстом
// «signal: killed», и отличить остановку от негодной записи по самой ошибке
// нельзя — только по контексту шага.
type killedConverter struct {
	cancel func()
}

func (c *killedConverter) Convert(ctx context.Context, _, _ string) error {
	c.cancel()
	<-ctx.Done()
	return errors.New("ffmpeg conversion failed: signal: killed")
}

// Критерий приёмки 9. Остановка сервиса посреди приведения не выносит записи
// приговора и **не тратит отказа**: запись не виновата в том, что нас
// перезапустили, и несколько выкладок подряд иначе останавливают здоровую
// многочасовую запись с приговором «попытки исчерпаны».
func TestShutdownDuringConversionKeepsRecordRetryable(t *testing.T) {
	ctx, cancel := context.WithCancel(t.Context())
	defer cancel()

	converter := &killedConverter{cancel: cancel}
	env := newPipelineEnv(t, &okMetaViewer{}, converter)
	record := newRecord(t, env)

	err := env.service.RunStep(ctx)

	require.Error(t, err, "шаг обязан сообщить об обрыве наверх")
	require.ErrorIs(t, err, context.Canceled, "обрыв узнаётся по смыслу, а не по тексту")

	after := readRecord(t, env, record.Id)

	assert.Equal(t, entity.StateUploaded, after.State, "запись осталась на повтор")
	assert.False(t, after.IsHalted(), "приговора не выносили")
	assert.Nil(t, after.AcquisitionID, "захват снят: запись возьмёт следующий прогон")
	assert.Equal(t, 0, after.Attempts, "остановка отказа не тратит")
	assert.Nil(t, after.ErrorText)
}

// Запись, которую шаг не успел взять, потому что нас уже остановили, остаётся
// нетронутой: захват не случился, отказ не потрачен.
func TestShutdownBeforeStepLeavesRecordUntouched(t *testing.T) {
	env := newPipelineEnv(t, &okMetaViewer{}, &failingConverter{})
	record := newRecord(t, env)

	ctx, cancel := context.WithCancel(t.Context())
	cancel()

	err := env.service.RunStep(ctx)

	// Исход «шаг не сделал ничего» — это `NoopJobError`: воркер не пишет о нём
	// владельцу и не считает его в метрику.
	require.Error(t, err)
	var noop *contract.NoopJobError
	require.ErrorAs(t, err, &noop)

	after := readRecord(t, env, record.Id)
	assert.Equal(t, entity.StateUploaded, after.State)
	assert.Equal(t, 0, after.Attempts, "захвата не было — отказу взяться неоткуда")
	assert.Nil(t, after.AcquisitionID)
}
