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

package setting

import (
	"path/filepath"
	"strings"
	"testing"

	"forgejo.org/modules/json"
	"forgejo.org/modules/log"
	"forgejo.org/modules/test"

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

func initLoggersByConfig(t *testing.T, config string) (*log.LoggerManager, func()) {
	defer test.MockVariableValue(&Log, LogGlobalConfig{})()

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

	manager := log.NewManager()
	initManagedLoggers(manager, cfg)
	return manager, manager.Close
}

func initLoggerConfig(t *testing.T, config string) ConfigProvider {
	defer test.MockVariableValue(&Log, LogGlobalConfig{})()

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

	prepareLoggerConfig(cfg)

	return cfg
}

func toJSON(v any) string {
	b, _ := json.MarshalIndent(v, "", "\t")
	return string(b)
}

func TestLogConfigDefault(t *testing.T) {
	manager, managerClose := initLoggersByConfig(t, ``)
	defer managerClose()

	writerDump := `
{
	"console": {
		"BufferLen": 10000,
		"Colorize": false,
		"Expression": "",
		"Exclusion": "",
		"Flags": "stdflags",
		"Level": "info",
		"Prefix": "",
		"StacktraceLevel": "none",
		"WriterOption": {
			"Stderr": false
		},
		"WriterType": "console"
	}
}
`

	dump := manager.GetLogger(log.DEFAULT).DumpWriters()
	require.JSONEq(t, writerDump, toJSON(dump))

	dump = manager.GetLogger("access").DumpWriters()
	require.JSONEq(t, "{}", toJSON(dump))

	dump = manager.GetLogger("router").DumpWriters()
	require.JSONEq(t, writerDump, toJSON(dump))

	dump = manager.GetLogger("xorm").DumpWriters()
	require.JSONEq(t, writerDump, toJSON(dump))
}

func TestLogConfigDisable(t *testing.T) {
	manager, managerClose := initLoggersByConfig(t, `
[log]
logger.router.MODE =
logger.xorm.MODE =
`)
	defer managerClose()

	writerDump := `
{
	"console": {
		"BufferLen": 10000,
		"Colorize": false,
		"Expression": "",
		"Exclusion": "",
		"Flags": "stdflags",
		"Level": "info",
		"Prefix": "",
		"StacktraceLevel": "none",
		"WriterOption": {
			"Stderr": false
		},
		"WriterType": "console"
	}
}
`

	dump := manager.GetLogger(log.DEFAULT).DumpWriters()
	require.JSONEq(t, writerDump, toJSON(dump))

	dump = manager.GetLogger("access").DumpWriters()
	require.JSONEq(t, "{}", toJSON(dump))

	dump = manager.GetLogger("router").DumpWriters()
	require.JSONEq(t, "{}", toJSON(dump))

	dump = manager.GetLogger("xorm").DumpWriters()
	require.JSONEq(t, "{}", toJSON(dump))
}

func TestLogConfigLegacyDefault(t *testing.T) {
	manager, managerClose := initLoggersByConfig(t, `
[log]
MODE = console
`)
	defer managerClose()

	writerDump := `
{
	"console": {
		"BufferLen": 10000,
		"Colorize": false,
		"Expression": "",
		"Exclusion": "",
		"Flags": "stdflags",
		"Level": "info",
		"Prefix": "",
		"StacktraceLevel": "none",
		"WriterOption": {
			"Stderr": false
		},
		"WriterType": "console"
	}
}
`

	dump := manager.GetLogger(log.DEFAULT).DumpWriters()
	require.JSONEq(t, writerDump, toJSON(dump))

	dump = manager.GetLogger("access").DumpWriters()
	require.JSONEq(t, "{}", toJSON(dump))

	dump = manager.GetLogger("router").DumpWriters()
	require.JSONEq(t, writerDump, toJSON(dump))

	dump = manager.GetLogger("xorm").DumpWriters()
	require.JSONEq(t, writerDump, toJSON(dump))
}

func TestLogConfigLegacyMode(t *testing.T) {
	tempDir := t.TempDir()

	tempPath := func(file string) string {
		return filepath.Join(tempDir, file)
	}

	manager, managerClose := initLoggersByConfig(t, `
[log]
ROOT_PATH = `+tempDir+`
MODE = file
ROUTER = file
ACCESS = file
`)
	defer managerClose()

	writerDump := `
{
	"file": {
		"BufferLen": 10000,
		"Colorize": false,
		"Expression": "",
		"Exclusion": "",
		"Flags": "stdflags",
		"Level": "info",
		"Prefix": "",
		"StacktraceLevel": "none",
		"WriterOption": {
			"Compress": true,
			"CompressionLevel": -1,
			"DailyRotate": true,
			"FileName": "$FILENAME",
			"LogRotate": true,
			"MaxDays": 7,
			"MaxSize": 268435456
		},
		"WriterType": "file"
	}
}
`
	writerDumpAccess := `
{
	"file.access": {
		"BufferLen": 10000,
		"Colorize": false,
		"Expression": "",
		"Exclusion": "",
		"Flags": "none",
		"Level": "info",
		"Prefix": "",
		"StacktraceLevel": "none",
		"WriterOption": {
			"Compress": true,
			"CompressionLevel": -1,
			"DailyRotate": true,
			"FileName": "$FILENAME",
			"LogRotate": true,
			"MaxDays": 7,
			"MaxSize": 268435456
		},
		"WriterType": "file"
	}
}
`
	dump := manager.GetLogger(log.DEFAULT).DumpWriters()
	require.JSONEq(t, strings.ReplaceAll(writerDump, "$FILENAME", tempPath("gitea.log")), toJSON(dump))

	dump = manager.GetLogger("access").DumpWriters()
	require.JSONEq(t, strings.ReplaceAll(writerDumpAccess, "$FILENAME", tempPath("access.log")), toJSON(dump))

	dump = manager.GetLogger("router").DumpWriters()
	require.JSONEq(t, strings.ReplaceAll(writerDump, "$FILENAME", tempPath("gitea.log")), toJSON(dump))
}

func TestLogConfigLegacyModeDisable(t *testing.T) {
	manager, managerClose := initLoggersByConfig(t, `
[log]
ROUTER = file
ACCESS = file
DISABLE_ROUTER_LOG = true
ENABLE_ACCESS_LOG = false
`)
	defer managerClose()

	dump := manager.GetLogger("access").DumpWriters()
	require.JSONEq(t, "{}", toJSON(dump))

	dump = manager.GetLogger("router").DumpWriters()
	require.JSONEq(t, "{}", toJSON(dump))
}

func TestLogConfigNewConfig(t *testing.T) {
	manager, managerClose := initLoggersByConfig(t, `
[log]
LOGGER_ACCESS_MODE = console
LOGGER_XORM_MODE = console, console-1

[log.console]
LEVEL = warn

[log.console-1]
MODE = console
LEVEL = error
STDERR = true
`)
	defer managerClose()

	writerDump := `
{
	"console": {
		"BufferLen": 10000,
		"Colorize": false,
		"Expression": "",
		"Exclusion": "",
		"Flags": "stdflags",
		"Level": "warn",
		"Prefix": "",
		"StacktraceLevel": "none",
		"WriterOption": {
			"Stderr": false
		},
		"WriterType": "console"
	},
	"console-1": {
		"BufferLen": 10000,
		"Colorize": false,
		"Expression": "",
		"Exclusion": "",
		"Flags": "stdflags",
		"Level": "error",
		"Prefix": "",
		"StacktraceLevel": "none",
		"WriterOption": {
			"Stderr": true
		},
		"WriterType": "console"
	}
}
`
	writerDumpAccess := `
{
	"console.access": {
		"BufferLen": 10000,
		"Colorize": false,
		"Expression": "",
		"Exclusion": "",
		"Flags": "none",
		"Level": "warn",
		"Prefix": "",
		"StacktraceLevel": "none",
		"WriterOption": {
			"Stderr": false
		},
		"WriterType": "console"
	}
}
`
	dump := manager.GetLogger("xorm").DumpWriters()
	require.JSONEq(t, writerDump, toJSON(dump))

	dump = manager.GetLogger("access").DumpWriters()
	require.JSONEq(t, writerDumpAccess, toJSON(dump))
}

func TestLogConfigModeFile(t *testing.T) {
	tempDir := t.TempDir()

	tempPath := func(file string) string {
		return filepath.Join(tempDir, file)
	}

	manager, managerClose := initLoggersByConfig(t, `
[log]
ROOT_PATH = `+tempDir+`
BUFFER_LEN = 10
MODE = file, file1

[log.file1]
MODE = file
LEVEL = error
STACKTRACE_LEVEL = fatal
EXPRESSION = filter
EXCLUSION = not
FLAGS = medfile
PREFIX = "[Prefix] "
FILE_NAME = file-xxx.log
LOG_ROTATE = false
MAX_SIZE_SHIFT = 1
DAILY_ROTATE = false
MAX_DAYS = 90
COMPRESS = false
COMPRESSION_LEVEL = 4
`)
	defer managerClose()

	writerDump := `
{
	"file": {
		"BufferLen": 10,
		"Colorize": false,
		"Expression": "",
		"Exclusion": "",
		"Flags": "stdflags",
		"Level": "info",
		"Prefix": "",
		"StacktraceLevel": "none",
		"WriterOption": {
			"Compress": true,
			"CompressionLevel": -1,
			"DailyRotate": true,
			"FileName": "$FILENAME-0",
			"LogRotate": true,
			"MaxDays": 7,
			"MaxSize": 268435456
		},
		"WriterType": "file"
	},
	"file1": {
		"BufferLen": 10,
		"Colorize": false,
		"Expression": "filter",
		"Exclusion": "not",
		"Flags": "medfile",
		"Level": "error",
		"Prefix": "[Prefix] ",
		"StacktraceLevel": "fatal",
		"WriterOption": {
			"Compress": false,
			"CompressionLevel": 4,
			"DailyRotate": false,
			"FileName": "$FILENAME-1",
			"LogRotate": false,
			"MaxDays": 90,
			"MaxSize": 2
		},
		"WriterType": "file"
	}
}
`

	dump := manager.GetLogger(log.DEFAULT).DumpWriters()
	expected := writerDump
	expected = strings.ReplaceAll(expected, "$FILENAME-0", tempPath("gitea.log"))
	expected = strings.ReplaceAll(expected, "$FILENAME-1", tempPath("file-xxx.log"))
	require.JSONEq(t, expected, toJSON(dump))
}

func TestLegacyLoggerMigrations(t *testing.T) {
	type Cases = []struct {
		name string
		cfg  string
		exp  string
	}

	runCases := func(t *testing.T, key string, cases Cases) {
		for _, c := range cases {
			t.Run(c.name, func(t *testing.T) {
				cfg := initLoggerConfig(t, c.cfg)
				require.Equal(t, c.exp, cfg.Section("log").Key(key).String())
			})
		}
	}

	t.Run("default", func(t *testing.T) {
		runCases(t, "LOGGER_DEFAULT_MODE", Cases{
			{
				"uses default value for default logger",
				"",
				",",
			},
			{
				"uses logger.default.MODE for default logger",
				`[log]
logger.default.MODE = file
`,
				"file",
			},
		})
	})

	t.Run("access", func(t *testing.T) {
		runCases(t, "LOGGER_ACCESS_MODE", Cases{
			{
				"uses default value for access logger",
				"",
				"",
			},
			{
				"uses ACCESS for access logger",
				`[log]
ACCESS = file
`,
				"file",
			},
			{
				"ENABLE_ACCESS_LOG=true doesn't change access logger",
				`[log]
ENABLE_ACCESS_LOG = true
logger.access.MODE = console
`,
				"console",
			},
			{
				"ENABLE_ACCESS_LOG=false disables access logger",
				`[log]
ENABLE_ACCESS_LOG = false
logger.access.MODE = console
`,
				"",
			},
			{
				"logger.access.MODE has precedence over ACCESS for access logger",
				`[log]
ACCESS = file
logger.access.MODE = console
`,
				"console",
			},
			{
				"LOGGER_ACCESS_MODE has precedence over logger.access.MODE for access logger",
				`[log]
LOGGER_ACCESS_MODE = file
logger.access.MODE = console
`,
				"file",
			},
			{
				"ENABLE_ACCESS_LOG doesn't enable access logger",
				`[log]
ENABLE_ACCESS_LOG = true
`,
				"", // should be `,`
			},
		})
	})

	t.Run("router", func(t *testing.T) {
		runCases(t, "LOGGER_ROUTER_MODE", Cases{
			{
				"uses default value for router logger",
				"",
				",",
			},
			{
				"uses ROUTER for router logger",
				`[log]
ROUTER = file
`,
				"file",
			},
			{
				"DISABLE_ROUTER_LOG=false doesn't change router logger",
				`[log]
ROUTER = file
DISABLE_ROUTER_LOG = false
`,
				"file",
			},
			{
				"DISABLE_ROUTER_LOG=true disables router logger",
				`[log]
DISABLE_ROUTER_LOG = true
logger.router.MODE = console
`,
				"",
			},
			{
				"logger.router.MODE as precedence over ROUTER for router logger",
				`[log]
ROUTER = file
logger.router.MODE = console
`,
				"console",
			},
			{
				"LOGGER_ROUTER_MODE has precedence over logger.router.MODE for router logger",
				`[log]
LOGGER_ROUTER_MODE = file
logger.router.MODE = console
`,
				"file",
			},
		})
	})

	t.Run("xorm", func(t *testing.T) {
		runCases(t, "LOGGER_XORM_MODE", Cases{
			{
				"uses default value for xorm logger",
				"",
				",",
			},
			{
				"uses XORM for xorm logger",
				`[log]
XORM = file
`,
				"file",
			},
			{
				"ENABLE_XORM_LOG=true doesn't change xorm logger",
				`[log]
ENABLE_XORM_LOG = true
logger.xorm.MODE = console
`,
				"console",
			},
			{
				"ENABLE_XORM_LOG=false disables xorm logger",
				`[log]
ENABLE_XORM_LOG = false
logger.xorm.MODE = console
`,
				"",
			},
			{
				"logger.xorm.MODE has precedence over XORM for xorm logger",
				`[log]
XORM = file
logger.xorm.MODE = console
`,
				"console",
			},
			{
				"LOGGER_XORM_MODE has precedence over logger.xorm.MODE for xorm logger",
				`[log]
LOGGER_XORM_MODE = file
logger.xorm.MODE = console
`,
				"file",
			},
		})
	})

	t.Run("ssh", func(t *testing.T) {
		runCases(t, "LOGGER_SSH_MODE", Cases{
			{
				"uses default value for ssh logger",
				"",
				"",
			},
			{
				"deprecated config can enable logger",
				`[log]
ENABLE_SSH_LOG = true
`,
				",",
			},
			{
				"check priority",
				`[log]
LOGGER_SSH_MODE = file
ENABLE_SSH_LOG = true
`,
				"file",
			},
		})
	})
}
