package http

import (
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

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

// journalMounts — перечень адресного пространства для проверок журнала.
// Обработчиков он здесь не вешает: журналу нужны только границы, а не то, что
// стоит за ними.
func journalMounts() []Mount {
	return []Mount{
		{Path: AppRoot},
		{Path: HealthPath, Exact: true},
		{Path: MetricsPath, Exact: true},
	}
}

// journalRouteOf — маршрут строки журнала для названного пути.
func journalRouteOf(path string) string {
	return JournalRoute(httptest.NewRequest(http.MethodGet, path, http.NoBody), journalMounts())
}

// Путь, которым распоряжается спрашивающий, в журнал дословно не идёт — и
// принадлежность сервису тут ничего не меняет: путь под корнем приложения
// выбирает тот же аноним, что и путь вне корней.
func TestJournalRouteHidesRequestedPath(t *testing.T) {
	cases := []string{
		"/",
		"/records/abc123def456ghi",
		"/" + strings.Repeat("a", 1024),
		"/api/files/files/rec0000000000000/0f7b8dd3-d1cc-424c.mp3",
		"/_/",
		// Под корнем приложения — то же самое: образца с таким хвостом нет.
		"/app",
		"/app/",
		"/app/" + strings.Repeat("b", 1024),
		"/app/audiorecords/abc123def456ghi/file/" + strings.Repeat("c", 512),
		"/app/audiorecords/abc/def/ghi",
	}

	for _, path := range cases {
		assert.Equal(t, webappRoute, journalRouteOf(path), path)
	}
}

// Дословно пишется закрытый перечень: точные адреса наблюдения и образцы
// адресов приложения. Идентификатор записи в образец не входит — он приходит
// строкой обработчика полем `record_id`.
func TestJournalRouteKeepsClosedList(t *testing.T) {
	cases := map[string]string{
		"/app/audiorecords":                      AppRouteRecords,
		"/app/audiorecords/abc123def456ghi":      AppRouteRecord,
		"/app/audiorecords/abc123def456ghi/text": AppRouteRecordText,
		"/app/audiorecords/abc123def456ghi/file": AppRouteRecordFile,
		"/app/me":                                AppRouteMe,
		"/app/config":                            AppRouteConfig,
		HealthPath:                               HealthPath,
		MetricsPath:                              MetricsPath,
	}

	for path, want := range cases {
		assert.Equal(t, want, journalRouteOf(path), path)
	}
}

// Значение маршрута берётся из перечня образцов и ничего сверх него не
// возвращает: путь, приведённый маршрутизатором к другому виду, дословно уехать
// не может.
func TestJournalRouteAnswersOnlyFromClosedList(t *testing.T) {
	allowed := map[string]bool{webappRoute: true, HealthPath: true, MetricsPath: true}
	for _, pattern := range AppRoutePatterns {
		allowed[pattern] = true
	}

	cases := []string{
		"/app/me/",
		"/app/audiorecords/../me",
		"/app//me",
		"/app/audiorecords/{id}",
		"/app/me/" + strings.Repeat("d", 256),
	}

	for _, path := range cases {
		route := journalRouteOf(path)
		assert.True(t, allowed[route], "маршрут %q не принадлежит закрытому перечню (путь %q)", route, path)
	}
}

// Имя, под которым копия легла в каталог данных, в журнал не идёт: строка
// журнала иначе стала бы бессрочным ключом к чужой записи. Сегодня оно не
// попадает туда по построению — адрес копии назван идентификатором самой записи,
// — и проверка сторожит именно это.
func TestJournalCarriesNoStoredFileName(t *testing.T) {
	env := setupTestEnv(t, readableMetaViewer())

	created := httptest.NewRecorder()
	env.serve(created, createMultipartRequest(t, "разговор.mp3", []byte("данные")))
	require.Equal(t, http.StatusCreated, created.Code)

	recordID := intakeItemOf(t, created).ID
	require.Equal(t, http.StatusOK,
		env.getOwn("/app/audiorecords/"+recordID+"/file?"+CopyParam+"="+CopyOriginal).Code)

	names := storedFileNames(t, env)
	require.Len(t, names, 1)

	journal := env.journal.String()
	assert.NotContains(t, journal, names[0], "имя файла в каталоге данных уехало в журнал")
	assert.Contains(t, journal, recordID, "идентификатор записи остаётся: по нему прослеживается путь")
}

// Путь, отданный приложению, журнал заменяет исходом и длиной: строка о нём не
// растёт вместе с длиной пути.
func TestJournalWebappOutcomeInsteadOfPath(t *testing.T) {
	env := setupTestEnv(t, readableMetaViewer())

	long := "/" + strings.Repeat("щ", 500)
	require.Equal(t, http.StatusOK, env.get(long).Code)

	journal := env.journal.String()
	assert.NotContains(t, journal, strings.Repeat("щ", 500), "путь уехал в журнал дословно")
	assert.Contains(t, journal, "webapp.outcome="+OutcomeMarkup)
	assert.Contains(t, journal, "http.path_length=")
}

// Неузнанный запрос под корнем приложения журнал тоже не пишет дословно, а
// строка о нём не растёт вместе с длиной пути. Прежде путь под корнем считался
// принадлежащим сервису и уезжал в строку целиком — аноним писал в журнал
// владельца свой текст произвольной длины.
func TestJournalHidesAnonymousPathUnderAppRoot(t *testing.T) {
	env := setupTestEnv(t, readableMetaViewer())

	const filler = 4096
	long := "/app/" + strings.Repeat("щ", filler)
	require.Equal(t, http.StatusUnauthorized, env.get(long).Code)

	journal := env.journal.String()
	assert.NotContains(t, journal, strings.Repeat("щ", filler), "путь уехал в журнал дословно")
	assert.Contains(t, journal, "http.route="+webappRoute)
	assert.Contains(t, journal, "http.path_length=")
	assert.Less(t, len(journal), filler,
		"строка журнала растёт вместе с длиной запрошенного пути")
}
