package pocketbase

import (
	"strings"
	"testing"

	"github.com/google/uuid"
	"github.com/pocketbase/pocketbase/core"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase/migrations"
	"git.vakhrushev.me/av/transcriber/internal/clock"
	"git.vakhrushev.me/av/transcriber/internal/contract"
	"git.vakhrushev.me/av/transcriber/internal/entity"
)

// Страж удаления учётной записи — единственное, что стоит между владельцем
// панели и молчаливым обезличиванием чужого архива: при выключенном каскаде
// хранилище снимает ссылку и сохраняет запись без проверок.

func newAccount(t *testing.T, app core.App) *core.Record {
	t.Helper()

	users, err := app.FindCollectionByNameOrId(migrations.UsersCollection)
	require.NoError(t, err)

	record := core.NewRecord(users)
	record.Set("email", uuid.NewString()+"@example.test")
	record.Set("verified", true)
	record.Set("password", uuid.NewString())
	require.NoError(t, app.Save(record))

	return record
}

// newRecordOf заводит аудиозапись названного владельца.
func newRecordOf(t *testing.T, app core.App, ownerID string) *entity.AudioRecord {
	t.Helper()

	record := &entity.AudioRecord{
		State:          entity.StateUploaded,
		StateEnteredAt: clock.Now(),
		Source:         entity.SourceApi,
		OwnerID:        ownerID,
	}
	require.NoError(t, NewAudioRecordRepository(app).Create(record))
	return record
}

// Учётная запись с архивом не удаляется, и отказ называет причину — иначе
// наружу приезжает подсказка библиотеки про обязательную связь, по которой
// владелец панели пойдёт удалять записи руками.
func TestGuardOwnerDeletion(t *testing.T) {
	app := newTestStorage(t)

	account := newAccount(t, app)
	record := newRecordOf(t, app, account.Id)

	err := app.Delete(account)
	require.Error(t, err, "учётная запись с архивом не удаляется")
	assert.Contains(t, err.Error(), "остались записи", "отказ называет причину")

	after, err := NewAudioRecordRepository(app).Get(record.Id)
	require.NoError(t, err, "запись на месте")
	assert.Equal(t, account.Id, after.OwnerID, "и владелец у неё прежний")
}

// Считаются все коллекции с владельцем, а не одни записи: файл переживает свою
// запись, а тема живёт в словаре человека.
func TestGuardOwnerDeletionCountsEveryOwnedCollection(t *testing.T) {
	cases := map[string]func(t *testing.T, app core.App, ownerID string){
		"аудиозапись": func(t *testing.T, app core.App, ownerID string) {
			newRecordOf(t, app, ownerID)
		},
		"один файл без записи": func(t *testing.T, app core.App, ownerID string) {
			repo := NewFileRepository(app)
			work, err := repo.Stage(".mp3", strings.NewReader("запись"))
			require.NoError(t, err)
			defer func() { require.NoError(t, work.Close()) }()

			_, err = repo.Create("sample.mp3", work, contract.FileMeta{Format: "mp3"}, ownerID)
			require.NoError(t, err)
		},
		"одна тема словаря": func(t *testing.T, app core.App, ownerID string) {
			topics, err := app.FindCollectionByNameOrId(migrations.TopicsCollection)
			require.NoError(t, err)

			topic := core.NewRecord(topics)
			topic.Set("owner", ownerID)
			topic.Set("name", "личная тема")
			require.NoError(t, app.Save(topic))
		},
	}

	for name, own := range cases {
		t.Run(name, func(t *testing.T) {
			app := newTestStorage(t)
			account := newAccount(t, app)
			own(t, app, account.Id)

			err := app.Delete(account)
			require.Error(t, err, "учётная запись с этим добром не удаляется")
			assert.Contains(t, err.Error(), "остались записи",
				"отказ наш, а не подсказка библиотеки про обязательную связь")
		})
	}
}

// Учётная запись, за которой ничего не числится, удаляется штатно: страж
// заведён против потери архива, а не против удаления вообще.
func TestGuardOwnerDeletionLetsEmptyAccountGo(t *testing.T) {
	app := newTestStorage(t)

	account := newAccount(t, app)
	require.NoError(t, app.Delete(account), "пустая учётная запись удаляется")
}

// Колонка владельца пустого значения не принимает и умолчания не имеет:
// ничьей записи в хранилище не бывает, и завести её нечем — ни приёмом, ни
// конвейером, ни рукой в панели.
func TestOwnerColumnRefusesEmptyValue(t *testing.T) {
	app := newTestStorage(t)

	for _, name := range []string{migrations.RecordsCollection, migrations.FilesCollection} {
		t.Run(name, func(t *testing.T) {
			collection, err := app.FindCollectionByNameOrId(name)
			require.NoError(t, err)

			field := collection.Fields.GetByName("owner")
			require.NotNil(t, field, "колонка владельца заведена")

			relation, ok := field.(*core.RelationField)
			require.True(t, ok, "владелец — связь с учётной записью, а не строка")
			assert.True(t, relation.Required, "пустое значение колонка не принимает")
			assert.False(t, relation.CascadeDelete, "удаление учётной записи не уносит архив следом")
		})
	}
}

// Та же норма со стороны сохранения: схема отвергает запись без владельца, а не
// только объявляет колонку обязательной.
func TestStorageRefusesRecordWithoutOwner(t *testing.T) {
	app := newTestStorage(t)

	record := &entity.AudioRecord{
		State:          entity.StateUploaded,
		StateEnteredAt: clock.Now(),
		Source:         entity.SourceApi,
	}

	require.Error(t, NewAudioRecordRepository(app).Create(record),
		"ничья запись в хранилище не ложится")
}

// И файл — наравне с записью: разное правило у них читалось бы как недосмотр.
func TestStorageRefusesFileWithoutOwner(t *testing.T) {
	app := newTestStorage(t)

	repo := NewFileRepository(app)
	work, err := repo.Stage(".mp3", strings.NewReader("запись"))
	require.NoError(t, err)
	defer func() { require.NoError(t, work.Close()) }()

	_, err = repo.Create("sample.mp3", work, contract.FileMeta{Format: "mp3"}, "")
	require.Error(t, err, "ничей файл в хранилище не ложится")
}
