package config

import (
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"
)

// Проверка входа — единственная страховка от того, чтобы сервис поднялся с
// молча выключенным входом, то есть открытым наружу. До этих проверок она не
// исполнялась ни разу.

func validAuthConfig() AuthConfig {
	return AuthConfig{
		AuthURL:      "https://auth.example.com/api/oidc/authorization",
		TokenURL:     "https://auth.example.com/api/oidc/token",
		UserInfoURL:  "https://auth.example.com/api/oidc/userinfo",
		ClientID:     "transcriber",
		ClientSecret: "secret-value",
		RedirectURL:  "https://transcriber.example.com/auth/callback",
	}
}

func TestAuthConfigValidateAcceptsFilled(t *testing.T) {
	if err := validAuthConfig().Validate(); err != nil {
		t.Fatalf("заполненный конфиг отвергнут: %v", err)
	}
}

func TestAuthConfigValidateNamesEveryMissingKey(t *testing.T) {
	cases := map[string]func(*AuthConfig){
		"auth_url":      func(c *AuthConfig) { c.AuthURL = "" },
		"token_url":     func(c *AuthConfig) { c.TokenURL = "" },
		"user_info_url": func(c *AuthConfig) { c.UserInfoURL = "" },
		"client_id":     func(c *AuthConfig) { c.ClientID = "" },
		"client_secret": func(c *AuthConfig) { c.ClientSecret = "" },
		"redirect_url":  func(c *AuthConfig) { c.RedirectURL = "" },
	}

	for key, clear := range cases {
		t.Run(key, func(t *testing.T) {
			cfg := validAuthConfig()
			clear(&cfg)

			err := cfg.Validate()
			if err == nil {
				t.Fatalf("пустой ключ %s пропущен — сервис поднимется с выключенным входом", key)
			}
			if !strings.Contains(err.Error(), key) {
				t.Fatalf("имя ключа %s не названо: %v", key, err)
			}
		})
	}
}

// TestAuthConfigValidateHidesSecretValue: сообщение об отказе уезжает в журнал,
// и значения секрета в нём быть не должно — только имя ключа.
func TestAuthConfigValidateHidesSecretValue(t *testing.T) {
	cfg := validAuthConfig()
	cfg.ClientSecret = "super-secret-value"
	cfg.AuthURL = ""

	err := cfg.Validate()
	if err == nil {
		t.Fatal("отказа нет")
	}
	if strings.Contains(err.Error(), "super-secret-value") {
		t.Fatalf("значение секрета попало в текст отказа: %v", err)
	}
}

// TestAuthConfigValidateRejectsMalformedURL: непустая строка, не похожая на
// адрес, отвергается здесь, а не позже — хранилище отказало бы уже из хука
// подъёма, до регистрации пробы здоровья, и сервис упал бы молча целиком.
func TestAuthConfigValidateRejectsMalformedURL(t *testing.T) {
	cases := map[string]string{
		"без схемы":      "auth.example.com/api/oidc/authorization",
		"пробел спереди": " https://auth.example.com/authorize",
		"чужая схема":    "ftp://auth.example.com/authorize",
		"пустой хост":    "https:///authorize",
		"не адрес вовсе": "todo: заполнить",
	}

	for name, value := range cases {
		t.Run(name, func(t *testing.T) {
			cfg := validAuthConfig()
			cfg.AuthURL = value

			err := cfg.Validate()
			if err == nil {
				t.Fatalf("негодный адрес %q пропущен", value)
			}
			if !strings.Contains(err.Error(), "auth_url") {
				t.Fatalf("имя ключа не названо: %v", err)
			}
		})
	}
}

func writeConfig(t *testing.T, body string) string {
	t.Helper()

	path := filepath.Join(t.TempDir(), "config.toml")
	if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
		t.Fatalf("не удалось записать файл настроек: %v", err)
	}
	return path
}

const validConfigBody = `
[storage]
data_dir = "data"
`

// Инвариант «секрет не покидает конфиг»: текст отказа разбора собирает чужая
// библиотека из разбираемого куска файла, и оборванная строка ключа доступа
// уехала бы в журнал вместе со значением. Отсюда собственное сообщение.
func TestLoadConfigMalformedSecretLineHidesValue(t *testing.T) {
	const secret = "AQVNfake-secret-api-key-value"
	// Кавычка не закрыта: разбор оборвётся на значении.
	path := writeConfig(t, "[yandex]\nfolder_id = \"b1g\"\nspeech_kit_api_key = \""+secret+"\n")

	_, err := LoadConfig(path)
	if err == nil {
		t.Fatal("поломанный файл настроек принят")
	}

	message := err.Error()
	for _, part := range []string{secret, "AQVNfake", "secret-api-key-value"} {
		if strings.Contains(message, part) {
			t.Fatalf("значение ключа доступа уехало в отказ: %v", err)
		}
	}
	// Без места и ключа отказ нечинибелен: скрыть значение мало.
	if !strings.Contains(message, "строке 3") {
		t.Fatalf("номер строки не назван, чинить нечего: %v", err)
	}
	if !strings.Contains(message, "speech_kit_api_key") {
		t.Fatalf("ключ не назван, чинить нечего: %v", err)
	}
}

// Обратная сторона того же разреза: отказ несовпадения типов собран из имён
// ключей и типов, значений в нём нет, и выбрасывать его текст незачем.
func TestLoadConfigTypeMismatchKeepsDiagnostics(t *testing.T) {
	path := writeConfig(t, "[server]\nport = \"8080\"\n"+validConfigBody)

	_, err := LoadConfig(path)
	if err == nil {
		t.Fatal("строка вместо числа принята")
	}
	if !strings.Contains(err.Error(), "port") {
		t.Fatalf("имя ключа не названо, чинить нечего: %v", err)
	}
}

// Вторая ветка разреза: разбор оборвался до всякого ключа, и последнего ключа
// нет вовсе. Пропущенная скобка секции — обычная опечатка, а ветка эта самая
// уязвимая: именно в ней будущая правка легче всего протащит текст библиотеки
// обратно.
func TestLoadConfigMalformedBeforeAnyKeyHidesValue(t *testing.T) {
	const secret = "AQVNsecret-api-key-value"
	// У секции не закрыта скобка: разбор оборвётся, не назвав ни одного ключа.
	path := writeConfig(t, "[yandex\nfolder_id = \"b1g\"\nspeech_kit_api_key = \""+secret+"\"\n")

	_, err := LoadConfig(path)
	if err == nil {
		t.Fatal("поломанный файл настроек принят")
	}

	message := err.Error()
	for _, part := range []string{secret, "AQVNsecret", "secret-api-key-value"} {
		if strings.Contains(message, part) {
			t.Fatalf("значение ключа доступа уехало в отказ: %v", err)
		}
	}
	if !strings.Contains(message, "строке") {
		t.Fatalf("место отказа не названо, чинить нечего: %v", err)
	}
}

// Числа конвейера приезжают из файла, а не выдумываются кодом.
func TestLoadConfigReadsPipelineSettings(t *testing.T) {
	path := writeConfig(t, `
[pipeline]
workers = 7
own_work_limit_minutes = 90
foreign_work_limit_minutes = 720

[storage]
data_dir = "data"
`)

	cfg, err := LoadConfig(path)
	if err != nil {
		t.Fatalf("конфиг не прочитан: %v", err)
	}

	if cfg.Pipeline.Workers != 7 {
		t.Errorf("число воркеров не прочитано: %d", cfg.Pipeline.Workers)
	}

	limits := cfg.Pipeline.StuckLimits()
	if limits.Own != 90*time.Minute {
		t.Errorf("предел своей работы не прочитан: %v", limits.Own)
	}
	if limits.Foreign != 720*time.Minute {
		t.Errorf("предел чужой работы не прочитан: %v", limits.Foreign)
	}
}

// Умолчания есть у всех трёх чисел: файл без секции конвейера годен, и сервис
// поднимается с рабочими значениями.
func TestPipelineSettingsHaveDefaults(t *testing.T) {
	path := writeConfig(t, "[storage]\ndata_dir = \"data\"\n")

	cfg, err := LoadConfig(path)
	if err != nil {
		t.Fatalf("конфиг не прочитан: %v", err)
	}

	if cfg.Pipeline.Workers <= 0 {
		t.Errorf("умолчание числа воркеров негодно: %d", cfg.Pipeline.Workers)
	}
	if err := cfg.Pipeline.Validate(); err != nil {
		t.Errorf("умолчания не проходят собственную проверку: %v", err)
	}
}

// Ноль воркеров — объявленный режим, а отрицательное число и нулевой предел —
// опечатка: подниматься с ней значит остановить всякую запись первым же
// захватом.
func TestPipelineValidateSeparatesModeFromTypo(t *testing.T) {
	valid := PipelineConfig{Workers: 0, OwnWorkLimitMinutes: 60, ForeignWorkLimitMinutes: 1440}
	if err := valid.Validate(); err != nil {
		t.Errorf("ноль воркеров объявлен законным значением: %v", err)
	}

	for name, cfg := range map[string]PipelineConfig{
		"отрицательное число воркеров": {Workers: -1, OwnWorkLimitMinutes: 60, ForeignWorkLimitMinutes: 1440},
		"нулевой предел своей работы":  {Workers: 1, OwnWorkLimitMinutes: 0, ForeignWorkLimitMinutes: 1440},
		"нулевой предел чужой работы":  {Workers: 1, OwnWorkLimitMinutes: 60, ForeignWorkLimitMinutes: 0},
	} {
		if err := cfg.Validate(); err == nil {
			t.Errorf("%s принято за режим", name)
		}
	}
}
