package http

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"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/entity"
)

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

// newSecondAccount заводит вторую учётную запись с собственной сессией.
// Постоянный адрес почты первой занят, и повторное сохранение отвергается —
// адрес здесь свой.
func newSecondAccount(t *testing.T, app core.App) (*core.Record, string) {
	t.Helper()

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

	record := core.NewRecord(users)
	record.Set("email", "stranger@example.com")
	record.Set("verified", true)
	record.SetRandomPassword()
	require.NoError(t, app.Save(record))

	token, err := record.NewAuthToken()
	require.NoError(t, err)

	return record, token
}

// withSessionHeader предъявляет сессию заголовком. Собственная поверхность
// хранилища читается только так: слой, перекладывающий куку в заголовок, на неё
// намеренно не наведён — часть её защищена ровно тем, что браузер заголовка сам
// не шлёт.
func withSessionHeader(req *http.Request, session string) *http.Request {
	req.Header.Set("Authorization", session)
	return req
}

// serveAs шлёт запрос от имени названной сессии, а не сессии окружения.
func serveAs(env *testEnv, session string, w http.ResponseWriter, req *http.Request) {
	req.AddCookie(&http.Cookie{Name: SessionCookieName, Value: session})
	env.mux.ServeHTTP(w, req)
}

// Чужая задача неотличима от несуществующей: тот же код и то же тело. Разница
// ответов обратила бы опрос в перебор — по ней считывается, какие задачи
// заведены.
func TestGetTranscribeJobStatus_ForeignJobLooksMissing(t *testing.T) {
	env := setupTestEnv(t, readableMetaViewer())

	job := jobWithFile(t, env)
	_, stranger := newSecondAccount(t, env.app)

	foreign := httptest.NewRecorder()
	serveAs(env, stranger, foreign, httptest.NewRequest("GET", "/app/audiorecords/"+job.Id, http.NoBody))

	unknown := httptest.NewRecorder()
	serveAs(env, stranger, unknown, httptest.NewRequest("GET", "/app/audiorecords/unknown0000000000", http.NoBody))

	require.Equal(t, http.StatusNotFound, foreign.Code, "чужая задача не отдаётся")
	assert.Equal(t, unknown.Code, foreign.Code, "код тот же, что у неизвестного идентификатора")
	assert.JSONEq(t, unknown.Body.String(), foreign.Body.String(), "и тело то же")

	// Ни состояния, ни текста расшифровки в теле нет.
	assert.NotContains(t, foreign.Body.String(), entity.StateUploaded)
	assert.NotContains(t, foreign.Body.String(), "transcription_text")
}

// Владельцем принятой записи становится предъявитель сессии — и у задачи, и у
// её файла.
func TestCreateTranscribeJob_OwnerIsSession(t *testing.T) {
	env := setupTestEnv(t, readableMetaViewer())

	w := httptest.NewRecorder()
	env.serve(w, createMultipartRequest(t, "sample.mp3", []byte("запись")))
	require.Equal(t, http.StatusCreated, w.Code)

	response := intakeItemOf(t, w)

	record, err := env.app.FindRecordById(migrations.RecordsCollection, response.ID)
	require.NoError(t, err)
	assert.Equal(t, env.account.Id, record.GetString("owner"), "владелец задачи — предъявитель")

	fileRecord, err := env.app.FindRecordById("files", record.GetString("original_file"))
	require.NoError(t, err)
	assert.Equal(t, env.account.Id, fileRecord.GetString("owner"), "владелец файла — он же")
}

// Владельца не задают запросом: своё значение в форме на результат не влияет.
func TestCreateTranscribeJob_OwnerFieldFromRequestIgnored(t *testing.T) {
	env := setupTestEnv(t, readableMetaViewer())

	_, stranger := newSecondAccount(t, env.app)

	req := createMultipartRequest(t, "sample.mp3", []byte("запись"))
	query := req.URL.Query()
	query.Set("owner", stranger)
	req.URL.RawQuery = query.Encode()

	w := httptest.NewRecorder()
	env.serve(w, req)
	require.Equal(t, http.StatusCreated, w.Code)

	response := intakeItemOf(t, w)

	record, err := env.app.FindRecordById(migrations.RecordsCollection, response.ID)
	require.NoError(t, err)
	assert.Equal(t, env.account.Id, record.GetString("owner"))
}

// Предъявитель, чья сессия не даёт учётной записи пользователя, получает отказ
// до чтения тела. Владелец панели — именно такой: узнан он узнан, а записи в
// коллекции пользователей у него нет, и владельцем записи он стать не может.
//
// Отказ **до** укладки обязателен: позже пришлось бы убирать уже сохранённый
// файл, а уборки файлов сервис не умеет вовсе.
func TestCreateTranscribeJob_SuperuserSessionRejected(t *testing.T) {
	env := setupTestEnv(t, readableMetaViewer())

	superusers, err := env.app.FindCollectionByNameOrId(core.CollectionNameSuperusers)
	require.NoError(t, err)

	admin := core.NewRecord(superusers)
	admin.Set("email", "owner@example.com")
	admin.SetRandomPassword()
	require.NoError(t, env.app.Save(admin))

	token, err := admin.NewAuthToken()
	require.NoError(t, err)

	w := httptest.NewRecorder()
	serveAs(env, token, w, createMultipartRequest(t, "sample.mp3", []byte("запись")))

	assert.Equal(t, http.StatusForbidden, w.Code, "узнан, но не запись коллекции пользователей")
	assert.Equal(t, 0, countJobs(t, env), "задачи не заведено")
	assert.Equal(t, 0, countFiles(t, env), "и файла тоже")
}

// Чужой файл не отдаётся по ссылке, а свой отдаётся. Проверяется именно переход
// по ссылке: токен файла хранилище выдаёт на предъявителя, а не на файл, и отказ
// наступает на скачивании, где правило просмотра судит владельца. Проверка,
// написанная на выдачу токена, зеленела бы, не касаясь пути, по которому аудио и
// уходит.
func TestFileDownload_NarrowedByOwner(t *testing.T) {
	env := setupTestEnv(t, readableMetaViewer())

	job := jobWithFile(t, env)
	_, stranger := newSecondAccount(t, env.app)

	record, err := env.app.FindRecordById("files", *job.OriginalFileID)
	require.NoError(t, err)
	require.Equal(t, env.account.Id, record.GetString("owner"))

	link := "/api/files/files/" + record.Id + "/" + record.GetString("file")

	mine := httptest.NewRecorder()
	env.mux.ServeHTTP(mine, withSessionHeader(
		httptest.NewRequest("GET", link+"?token="+fileToken(t, env, env.session), http.NoBody), env.session))

	foreign := httptest.NewRecorder()
	env.mux.ServeHTTP(foreign, withSessionHeader(
		httptest.NewRequest("GET", link+"?token="+fileToken(t, env, stranger), http.NoBody), stranger))

	require.Equal(t, http.StatusOK, mine.Code, "свой файл отдаётся")
	assert.Equal(t, "запись", mine.Body.String(), "и отдаётся содержимым")

	assert.NotEqual(t, http.StatusOK, foreign.Code, "чужой файл не отдаётся")
	assert.NotContains(t, foreign.Body.String(), "запись", "содержимого в отказе нет")
}

// fileToken берёт у хранилища токен файла для названной сессии. Токен выдаётся
// на предъявителя: о файле хранилище при выдаче не спрашивает.
func fileToken(t *testing.T, env *testEnv, session string) string {
	t.Helper()

	w := httptest.NewRecorder()
	env.mux.ServeHTTP(w, withSessionHeader(
		httptest.NewRequest("POST", "/api/files/token", http.NoBody), session))
	require.Equal(t, http.StatusOK, w.Code, "токен файла выдаётся всякому вошедшему")

	var body struct {
		Token string `json:"token"`
	}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
	require.NotEmpty(t, body.Token)

	return body.Token
}
