// Copyright 2024 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package promslog

import (
	"bytes"
	"context"
	"fmt"
	"log/slog"
	"regexp"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
	"gopkg.in/yaml.v2"
)

var (
	slogStyleLogRegexp  = regexp.MustCompile(`(?P<TimeKey>time)=.*level=(?P<LevelValue>WARN|INFO|ERROR|DEBUG).*(?P<SourceKey>source)=.*`)
	goKitStyleLogRegexp = regexp.MustCompile(`(?P<TimeKey>ts)=.*level=(?P<LevelValue>warn|info|error|debug).*(?P<SourceKey>caller)=.*`)
)

// Make sure creating and using a logger with an empty configuration doesn't
// result in a panic.
func TestDefaultConfig(t *testing.T) {
	require.NotPanics(t, func() {
		logger := New(&Config{})
		logger.Info("empty config `Info()` test", "hello", "world")
		logger.Log(context.Background(), slog.LevelInfo, "empty config `Log()` test", "hello", "world")
		logger.LogAttrs(context.Background(), slog.LevelInfo, "empty config `LogAttrs()` test", slog.String("hello", "world"))
	})
}

func TestUnmarshallLevel(t *testing.T) {
	l := &AllowedLevel{}
	err := yaml.Unmarshal([]byte(`debug`), l)
	if err != nil {
		t.Error(err)
	}
	if l.s != "debug" {
		t.Errorf("expected %s, got %s", "debug", l.s)
	}
}

func TestUnmarshallEmptyLevel(t *testing.T) {
	l := &AllowedLevel{}
	err := yaml.Unmarshal([]byte(``), l)
	if err != nil {
		t.Error(err)
	}
	if l.s != "" {
		t.Errorf("expected empty level, got %s", l.s)
	}
}

func TestUnmarshallBadLevel(t *testing.T) {
	l := &AllowedLevel{}
	err := yaml.Unmarshal([]byte(`debugg`), l)
	if err == nil {
		t.Error("expected error")
	}
	expErr := `unrecognized log level debugg`
	if err.Error() != expErr {
		t.Errorf("expected error %s, got %s", expErr, err.Error())
	}
	if l.s != "" {
		t.Errorf("expected empty level, got %s", l.s)
	}
}

func getLogEntryLevelCounts(s string, re *regexp.Regexp) map[string]int {
	counters := make(map[string]int)
	lines := strings.Split(s, "\n")
	for _, line := range lines {
		matches := re.FindStringSubmatch(line)
		if len(matches) > 1 {
			levelIndex := re.SubexpIndex("LevelValue")

			counters[strings.ToLower(matches[levelIndex])]++
		}
	}

	return counters
}

func TestDynamicLevels(t *testing.T) {
	var buf bytes.Buffer
	wantedLevelCounts := map[string]int{"info": 1, "debug": 1}

	tests := map[string]struct {
		logStyle         LogStyle
		logStyleRegexp   *regexp.Regexp
		wantedLevelCount map[string]int
	}{
		"slog_log_style":   {logStyle: SlogStyle, logStyleRegexp: slogStyleLogRegexp, wantedLevelCount: wantedLevelCounts},
		"go-kit_log_style": {logStyle: GoKitStyle, logStyleRegexp: goKitStyleLogRegexp, wantedLevelCount: wantedLevelCounts},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			buf.Reset() // Ensure buf is reset prior to tests
			config := &Config{Writer: &buf, Style: tc.logStyle}
			logger := New(config)

			// Test that log level can be adjusted on-the-fly to debug and that a
			// log entry can be written to the file.
			err := config.Level.Set("debug")
			require.NoError(t, err)
			logger.Info("info", "hello", "world")
			logger.Debug("debug", "hello", "world")

			counts := getLogEntryLevelCounts(buf.String(), tc.logStyleRegexp)
			require.Equalf(t, tc.wantedLevelCount["info"], counts["info"], "info log successfully detected")
			require.Equalf(t, tc.wantedLevelCount["debug"], counts["debug"], "debug log successfully detected")
			// Print logs for humans to see, if needed.
			fmt.Println(buf.String())
			buf.Reset()

			// Test that log level can be adjusted on-the-fly to info and that a
			// subsequent call to write a debug level log is _not_ written to the
			// file.
			err = config.Level.Set("info")
			require.NoError(t, err)
			logger.Info("info", "hello", "world")
			logger.Debug("debug", "hello", "world")

			counts = getLogEntryLevelCounts(buf.String(), tc.logStyleRegexp)
			require.Equalf(t, tc.wantedLevelCount["info"], counts["info"], "info log successfully detected")
			require.NotEqualf(t, tc.wantedLevelCount["debug"], counts["debug"], "extra debug log detected")
			// Print logs for humans to see, if needed.
			fmt.Println(buf.String())
			buf.Reset()
		})
	}
}

func TestTruncateSourceFileName_DefaultStyle(t *testing.T) {
	var buf bytes.Buffer

	config := &Config{
		Writer: &buf,
	}

	logger := New(config)
	logger.Info("test message")

	output := buf.String()

	if !strings.Contains(output, "source=slog_test.go:") {
		t.Errorf("Expected source file name to be truncated to basename, got: %s", output)
	}

	if strings.Contains(output, "/") {
		t.Errorf("Expected no directory separators in source file name, got: %s", output)
	}
}

func TestTruncateSourceFileName_GoKitStyle(t *testing.T) {
	var buf bytes.Buffer

	config := &Config{
		Writer: &buf,
		Style:  GoKitStyle,
	}

	logger := New(config)
	logger.Info("test message")

	output := buf.String()

	// In GoKitStyle, the source key is "caller".
	if !strings.Contains(output, "caller=slog_test.go:") {
		t.Errorf("Expected caller to contain basename of source file, got: %s", output)
	}

	if strings.Contains(output, "/") {
		t.Errorf("Expected no directory separators in caller, got: %s", output)
	}
}
