package logger_test

import (
	"bytes"
	"errors"
	"fmt"
	"log/slog"
	"strings"
	"testing"
	"time"

	"github.com/imgproxy/imgproxy/v3/errctx"
	"github.com/imgproxy/imgproxy/v3/logger"
	"github.com/imgproxy/imgproxy/v3/testutil"
	"github.com/stretchr/testify/suite"
)

type FormatterStructuredTestSuite struct {
	testutil.LazySuite

	buf     testutil.LazyObj[*bytes.Buffer]
	config  testutil.LazyObj[*logger.Config]
	handler testutil.LazyObj[*logger.Handler]
	logger  testutil.LazyObj[*slog.Logger]
}

func (s *FormatterStructuredTestSuite) SetupTest() {
	s.buf, _ = testutil.NewLazySuiteObj(
		s,
		func() (*bytes.Buffer, error) {
			return new(bytes.Buffer), nil
		},
	)

	s.config, _ = testutil.NewLazySuiteObj(
		s,
		func() (*logger.Config, error) {
			cfg := logger.NewDefaultConfig()
			cfg.Format = logger.FormatStructured
			return &cfg, nil
		},
	)

	s.handler, _ = testutil.NewLazySuiteObj(
		s,
		func() (*logger.Handler, error) {
			return logger.NewHandler(s.buf(), s.config()), nil
		},
	)

	s.logger, _ = testutil.NewLazySuiteObj(
		s,
		func() (*slog.Logger, error) {
			return slog.New(s.handler()), nil
		},
	)
}

func (s *FormatterStructuredTestSuite) SetupSubTest() {
	s.ResetLazyObjects()
}

func (s *FormatterStructuredTestSuite) checkNextEntry(lvl, msg string) {
	str, err := s.buf().ReadString('\n')
	s.Require().NoError(err)

	const timeLen = 19 + 7  // +7 for key, separator, and quotes
	lvlLen := len(lvl) + 10 // +10 for key, separator, quotes, and spaces
	prefixLen := timeLen + lvlLen

	s.Require().GreaterOrEqual(len(str), prefixLen)

	timePart := str[:timeLen]
	levelPart := str[timeLen:prefixLen]

	now := time.Now()
	t, err := time.ParseInLocation(time.DateTime, timePart[6:timeLen-1], now.Location())
	s.Require().NoError(err)
	s.Require().WithinDuration(time.Now(), t, time.Minute)

	s.Equal(` level="`+lvl+`" `, levelPart)

	// Check the message
	s.Equal(msg+"\n", str[prefixLen:])
}

func (s *FormatterStructuredTestSuite) TestLevel() {
	type testEntry struct {
		level     slog.Level
		levelName string
		message   string
	}

	testEntries := []testEntry{
		{level: slog.LevelDebug, levelName: "DEBUG", message: "Debug message"},
		{level: slog.LevelInfo, levelName: "INFO", message: "Info message"},
		{level: slog.LevelWarn, levelName: "WARNING", message: "Warning message"},
		{level: slog.LevelError, levelName: "ERROR", message: "Error message"},
		{level: logger.LevelCritical, levelName: "CRITICAL", message: "Critical message"},
	}

	testCases := []struct {
		level   slog.Level
		entries []testEntry
	}{
		{level: slog.LevelDebug, entries: testEntries},
		{level: slog.LevelInfo, entries: testEntries[1:]},
		{level: slog.LevelWarn, entries: testEntries[2:]},
		{level: slog.LevelError, entries: testEntries[3:]},
		{level: logger.LevelCritical, entries: testEntries[4:]},
	}

	for _, tc := range testCases {
		s.Run(tc.level.String(), func() {
			s.config().Level = tc.level

			for _, entry := range testEntries {
				s.logger().Log(s.T().Context(), entry.level, entry.message)
			}

			for _, entry := range tc.entries {
				s.checkNextEntry(entry.levelName, fmt.Sprintf(`msg="%s"`, entry.message))
			}
		})
	}
}

func (s *FormatterStructuredTestSuite) TestAttributes() {
	s.logger().Info(
		"Test message",
		slog.String("string", "value"),
		slog.Int("int", -100),
		slog.Uint64("uint64", 200),
		slog.Float64("float64", 3.14),
		slog.Bool("bool", true),
		slog.Time("time", time.Date(1984, 1, 2, 3, 4, 5, 6, time.UTC)),
		slog.Duration("duration", time.Minute),
		slog.Any("err", errors.New("error value")),
		slog.Any("any", struct {
			Field1 string
			Field2 int
		}{"value", 42}),
	)

	s.checkNextEntry(
		"INFO",
		strings.Join([]string{
			`msg="Test message"`,
			`string="value"`,
			`int=-100`,
			`uint64=200`,
			`float64=3.14`,
			`bool=true`,
			`time="1984-01-02 03:04:05"`,
			`duration="1m0s"`,
			`err="error value"`,
			`any="{Field1:value Field2:42}"`,
		}, " "),
	)
}

func (s *FormatterStructuredTestSuite) TestGroups() {
	s.Run("LastGroupNotEmpty", func() {
		s.logger().
			With(
				slog.String("string", "value"),
				slog.Int("int", -100),
			).
			WithGroup("group1").
			With(
				slog.Uint64("uint64", 200),
				slog.Float64("float64", 3.14),
			).
			WithGroup("group2").
			With(slog.Group("group3",
				slog.Bool("bool", true),
				slog.Time("time", time.Date(1984, 1, 2, 3, 4, 5, 6, time.UTC)),
			)).
			With(slog.Group("empty_group")).
			WithGroup("group4").
			Info(
				"Test message",
				slog.Duration("duration", time.Minute),
				slog.Any("any", struct {
					Field1 string
					Field2 int
				}{"value", 42}),
			)

		s.checkNextEntry(
			"INFO",
			strings.Join([]string{
				`msg="Test message"`,
				`string="value"`,
				`int=-100`,
				`group1.uint64=200`,
				`group1.float64=3.14`,
				`group1.group2.group3.bool=true`,
				`group1.group2.group3.time="1984-01-02 03:04:05"`,
				`group1.group2.group4.duration="1m0s"`,
				`group1.group2.group4.any="{Field1:value Field2:42}"`,
			}, " "),
		)
	})

	s.Run("LastGroupsEmpty", func() {
		s.logger().
			With(
				slog.String("string", "value"),
				slog.Int("int", -100),
			).
			WithGroup("group1").
			With(
				slog.Uint64("uint64", 200),
				slog.Float64("float64", 3.14),
			).
			WithGroup("group2").
			With(slog.Group("group3",
				slog.Bool("bool", true),
				slog.Time("time", time.Date(1984, 1, 2, 3, 4, 5, 6, time.UTC)),
			)).
			With(slog.Group("empty_group")).
			WithGroup("group4").
			WithGroup("group5").
			Info("Test message")

		s.checkNextEntry(
			"INFO",
			strings.Join([]string{
				`msg="Test message"`,
				`string="value"`,
				`int=-100`,
				`group1.uint64=200`,
				`group1.float64=3.14`,
				`group1.group2.group3.bool=true`,
				`group1.group2.group3.time="1984-01-02 03:04:05"`,
			}, " "),
		)
	})
}

func (s *FormatterStructuredTestSuite) TestQuoting() {
	s.logger().Info(
		"Test message",
		"key", "value",
		"key with spaces", "value with spaces",
		`key"with"quotes`, `value"with"quotes`,
		"key\nwith\nnewlines", "value\nwith\nnewlines",
		slog.Group("group name", slog.String("key", "value")),
	)

	s.checkNextEntry(
		"INFO",
		strings.Join([]string{
			`msg="Test message"`,
			`key="value"`,
			`"key with spaces"="value with spaces"`,
			`"key\"with\"quotes"="value\"with\"quotes"`,
			`"key\nwith\nnewlines"="value\nwith\nnewlines"`,
			`"group name.key"="value"`,
		}, " "),
	)
}

func (s *FormatterStructuredTestSuite) TestSpecialFields() {
	s.logger().Info(
		"Test message",
		"stack", "stack value\nwith new lines",
		"key1", "value1",
		"error", errctx.NewTextError(
			"test", 0, errctx.WithDocsURL("http://example.com"),
		),
		"key2", "value2",
		"source", "source value",
		"key3", "value3",
		slog.Group(
			"group",
			"stack", "stack in group",
			"error", "error in group",
			"source", "source in group",
		),
	)

	s.checkNextEntry(
		"INFO",
		strings.Join([]string{
			`msg="Test message"`,
			`key1="value1"`,
			`key2="value2"`,
			`key3="value3"`,
			`group.stack="stack in group"`,
			`group.error="error in group"`,
			`group.source="source in group"`,
			`error="test"`,
			`error_docs_url="http://example.com"`,
			`source="source value"`,
			`stack="stack value\nwith new lines"`,
		}, " "),
	)
}

func TestFormatterStructured(t *testing.T) {
	suite.Run(t, new(FormatterStructuredTestSuite))
}
