// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package install

import (
	"bytes"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"

	"code.gitea.io/gitea/models/unittest"
	"code.gitea.io/gitea/modules/setting"
	"code.gitea.io/gitea/modules/test"
	"code.gitea.io/gitea/services/forms"

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

func TestRoutes(t *testing.T) {
	defer test.MockVariableValue(&setting.InstallLock, false)()

	r := Routes()
	assert.NotNil(t, r)

	w := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	r.ServeHTTP(w, req)
	assert.Equal(t, 200, w.Code)
	assert.Contains(t, w.Body.String(), `class="page-content install`)
	assert.Contains(t, w.Body.String(), `name="default_language"`)
	assert.Contains(t, w.Body.String(), `name="app_ini_file"`)

	w = httptest.NewRecorder()
	req = httptest.NewRequest(http.MethodGet, "/no-such", nil)
	r.ServeHTTP(w, req)
	assert.Equal(t, 404, w.Code)

	w = httptest.NewRecorder()
	req = httptest.NewRequest(http.MethodGet, "/assets/img/gitea.svg", nil)
	r.ServeHTTP(w, req)
	assert.Equal(t, 200, w.Code)
}

func TestReorderInstallLanguages(t *testing.T) {
	defer test.MockVariableValue(&setting.Langs, []string{"en-US", "de-DE", "fr-FR"})()

	assert.Equal(t, "en-US", resolveInstallDefaultLanguage(""))
	assert.Equal(t, "en-US", resolveInstallDefaultLanguage("invalid"))
	assert.Equal(t, "fr-FR", resolveInstallDefaultLanguage("fr-FR"))
	assert.Equal(t, []string{"fr-FR", "en-US", "de-DE"}, reorderInstallLanguages("fr-FR"))
	assert.Equal(t, []string{"en-US", "de-DE", "fr-FR"}, reorderInstallLanguages("invalid"))
}

func TestPopulateInstallFormFromConfig(t *testing.T) {
	defer test.MockVariableValue(&setting.SupportedDatabaseTypes, []string{"mysql", "postgres", "sqlite3"})()
	defer test.MockVariableValue(&setting.Langs, []string{"en-US", "de-DE", "fr-FR"})()

	cfg, err := setting.NewConfigProviderFromData(`
APP_NAME = Imported Gitea
RUN_USER = imported-user

[database]
DB_TYPE = postgres
HOST = db.example.com:5432
USER = gitea
PASSWD = secret
NAME = giteadb
SCHEMA = custom
SSL_MODE = require

[repository]
ROOT = /srv/gitea/repos

[server]
DOMAIN = gitea.example.com
SSH_PORT = 2222
HTTP_PORT = 4000
ROOT_URL = https://gitea.example.com/

[lfs]
PATH = /srv/gitea/lfs

[log]
ROOT_PATH = /srv/gitea/log

[mailer]
ENABLED = true
SMTP_ADDR = smtp.example.com
SMTP_PORT = 587
FROM = Gitea <gitea@example.com>
USER = smtp-user
PASSWD = smtp-pass

[service]
REGISTER_EMAIL_CONFIRM = true
REGISTER_MANUAL_CONFIRM = false
ENABLE_NOTIFY_MAIL = true
ADMIN_CREATED_ACCOUNT_MODE = invite
DISABLE_REGISTRATION = false
ALLOW_ONLY_INTERNAL_REGISTRATION = true
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
ENABLE_CAPTCHA = true
REQUIRE_SIGNIN_VIEW = true
DEFAULT_KEEP_EMAIL_PRIVATE = true
DEFAULT_ALLOW_CREATE_ORGANIZATION = false
DEFAULT_ENABLE_TIMETRACKING = false
NO_REPLY_ADDRESS = noreply.example.com

[openid]
ENABLE_OPENID_SIGNIN = true
ENABLE_OPENID_SIGNUP = true

[security]
ENABLE_UPDATE_CHECKER = true
PASSWORD_HASH_ALGO = pbkdf2

[admin]
ADMIN_MANAGEMENT_POLICY = super_admin_only

[i18n]
LANGS = de-DE,en-US
`)
	require.NoError(t, err)

	form, curDBType := newInstallFormFromSettings()
	curDBType = populateInstallFormFromConfig(&form, cfg, curDBType)

	assert.Equal(t, "postgres", curDBType)
	assert.Equal(t, "postgres", form.DbType)
	assert.Equal(t, "Imported Gitea", form.AppName)
	assert.Equal(t, "imported-user", form.RunUser)
	assert.Equal(t, "db.example.com:5432", form.DbHost)
	assert.Equal(t, "gitea", form.DbUser)
	assert.Equal(t, "secret", form.DbPasswd)
	assert.Equal(t, "giteadb", form.DbName)
	assert.Equal(t, "custom", form.DbSchema)
	assert.Equal(t, "require", form.SSLMode)
	assert.Equal(t, "/srv/gitea/repos", form.RepoRootPath)
	assert.Equal(t, "/srv/gitea/lfs", form.LFSRootPath)
	assert.Equal(t, "gitea.example.com", form.Domain)
	assert.Equal(t, 2222, form.SSHPort)
	assert.Equal(t, "4000", form.HTTPPort)
	assert.Equal(t, "https://gitea.example.com/", form.AppURL)
	assert.Equal(t, "/srv/gitea/log", form.LogRootPath)
	assert.Equal(t, "smtp.example.com", form.SMTPAddr)
	assert.Equal(t, "587", form.SMTPPort)
	assert.Equal(t, "Gitea <gitea@example.com>", form.SMTPFrom)
	assert.Equal(t, "smtp-user", form.SMTPUser)
	assert.Equal(t, "smtp-pass", form.SMTPPasswd)
	assert.Equal(t, "de-DE", form.DefaultLanguage)
	assert.True(t, form.RegisterConfirm)
	assert.False(t, form.RegisterManualConfirm)
	assert.True(t, form.MailNotify)
	assert.Equal(t, "invite", form.AdminCreatedAccountMode)
	assert.Equal(t, "local_only", form.RegistrationMode)
	assert.False(t, form.EnableOpenIDSignIn)
	assert.False(t, form.EnableOpenIDSignUp)
	assert.True(t, form.EnableCaptcha)
	assert.True(t, form.RequireSignInView)
	assert.True(t, form.DefaultKeepEmailPrivate)
	assert.False(t, form.DefaultAllowCreateOrganization)
	assert.False(t, form.DefaultEnableTimetracking)
	assert.Equal(t, "noreply.example.com", form.NoReplyAddress)
	assert.True(t, form.EnableUpdateChecker)
	assert.Equal(t, "pbkdf2", form.PasswordAlgorithm)
	assert.Equal(t, "super_admin_only", form.AdminManagementPolicy)
}

func TestPopulateInstallFormFromConfigWithSensitiveSecrets(t *testing.T) {
	cfg, err := setting.NewConfigProviderFromData(`
[server]
LFS_JWT_SECRET = lfs-secret

[security]
INTERNAL_TOKEN = internal-secret

[oauth2]
JWT_SECRET = oauth-secret
`)
	require.NoError(t, err)

	form, curDBType := newInstallFormFromSettings()
	form.ImportSensitiveSecrets = true
	populateInstallFormFromConfig(&form, cfg, curDBType)

	assert.Equal(t, "lfs-secret", form.ImportedLFSJWTSecret)
	assert.Equal(t, "internal-secret", form.ImportedInternalToken)
	assert.Equal(t, "oauth-secret", form.ImportedOAuth2JWTSecret)
}

func TestPopulateInstallFormFromConfigWithSensitiveSecretURIs(t *testing.T) {
	tmpDir := t.TempDir()
	lfsSecretPath := filepath.Join(tmpDir, "lfs_secret")
	internalTokenPath := filepath.Join(tmpDir, "internal_token")
	oauthSecretPath := filepath.Join(tmpDir, "oauth_secret")

	require.NoError(t, os.WriteFile(lfsSecretPath, []byte("lfs-secret-uri\n"), 0o644))
	require.NoError(t, os.WriteFile(internalTokenPath, []byte("internal-secret-uri\n"), 0o644))
	require.NoError(t, os.WriteFile(oauthSecretPath, []byte("oauth-secret-uri\n"), 0o644))

	cfg, err := setting.NewConfigProviderFromData(`
[server]
LFS_JWT_SECRET_URI = file:` + filepath.ToSlash(lfsSecretPath) + `

[security]
INTERNAL_TOKEN_URI = file:` + filepath.ToSlash(internalTokenPath) + `

[oauth2]
JWT_SECRET_URI = file:` + filepath.ToSlash(oauthSecretPath) + `
`)
	require.NoError(t, err)

	form, curDBType := newInstallFormFromSettings()
	form.ImportSensitiveSecrets = true
	populateInstallFormFromConfig(&form, cfg, curDBType)

	assert.Equal(t, "lfs-secret-uri", form.ImportedLFSJWTSecret)
	assert.Equal(t, "internal-secret-uri", form.ImportedInternalToken)
	assert.Equal(t, "oauth-secret-uri", form.ImportedOAuth2JWTSecret)
}

func TestPopulateInstallSensitiveSecretsFromConfigFillsMissingValuesOnly(t *testing.T) {
	tmpDir := t.TempDir()
	internalTokenPath := filepath.Join(tmpDir, "internal_token")
	require.NoError(t, os.WriteFile(internalTokenPath, []byte("internal-secret-uri\n"), 0o644))

	cfg, err := setting.NewConfigProviderFromData(`
[server]
LFS_JWT_SECRET = lfs-secret

[security]
INTERNAL_TOKEN_URI = file:` + filepath.ToSlash(internalTokenPath) + `

[oauth2]
JWT_SECRET = oauth-secret
`)
	require.NoError(t, err)

	form := forms.InstallForm{
		ImportSensitiveSecrets:  true,
		ImportedLFSJWTSecret:    "",
		ImportedInternalToken:   "",
		ImportedOAuth2JWTSecret: "already-set",
	}
	populateInstallSensitiveSecretsFromConfig(&form, cfg)

	assert.Equal(t, "lfs-secret", form.ImportedLFSJWTSecret)
	assert.Equal(t, "internal-secret-uri", form.ImportedInternalToken)
	assert.Equal(t, "already-set", form.ImportedOAuth2JWTSecret)
}

func TestImportAppINIWithSensitiveSecrets(t *testing.T) {
	defer test.MockVariableValue(&setting.InstallLock, false)()

	var body bytes.Buffer
	writer := multipart.NewWriter(&body)
	require.NoError(t, writer.WriteField("import_sensitive_secrets", "on"))

	fileWriter, err := writer.CreateFormFile("app_ini_file", "app.ini")
	require.NoError(t, err)
	_, err = fileWriter.Write([]byte(`
[server]
LFS_JWT_SECRET = lfs-secret

[security]
INTERNAL_TOKEN = internal-secret

[oauth2]
JWT_SECRET = oauth-secret
`))
	require.NoError(t, err)
	require.NoError(t, writer.Close())

	r := Routes()
	w := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodPost, "/import_app_ini", &body)
	req.Header.Set("Content-Type", writer.FormDataContentType())
	r.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code)
	assert.Contains(t, w.Body.String(), `name="imported_lfs_jwt_secret" value="lfs-secret"`)
	assert.Contains(t, w.Body.String(), `name="imported_internal_token" value="internal-secret"`)
	assert.Contains(t, w.Body.String(), `name="imported_o_auth2_jwt_secret" value="oauth-secret"`)
}

func TestApplyInstallSensitiveSecretsToConfigPersistsImportedValues(t *testing.T) {
	tmpDir := t.TempDir()
	configPath := filepath.Join(tmpDir, "app.ini")

	defer test.MockVariableValue(&setting.InternalToken, "")()

	cfg, err := setting.NewConfigProviderFromData("")
	require.NoError(t, err)

	form := forms.InstallForm{
		LFSRootPath:             filepath.Join(tmpDir, "lfs"),
		ImportSensitiveSecrets:  true,
		ImportedLFSJWTSecret:    "lfs-secret",
		ImportedInternalToken:   "internal-secret",
		ImportedOAuth2JWTSecret: "oauth-secret",
	}

	require.NoError(t, applyInstallSensitiveSecretsToConfig(cfg, &form))
	require.NoError(t, cfg.SaveTo(configPath))

	data, err := os.ReadFile(configPath)
	require.NoError(t, err)
	content := string(data)
	assert.Contains(t, content, "LFS_JWT_SECRET = lfs-secret")
	assert.Contains(t, content, "INTERNAL_TOKEN = internal-secret")
	assert.Contains(t, content, "JWT_SECRET = oauth-secret")
}

func TestMain(m *testing.M) {
	unittest.MainTest(m)
}
