package telemetry

import (
	"encoding/json"
	"fmt"
	"sync/atomic"
	"testing"
	"time"

	"github.com/grafana/loki/pkg/logproto"

	"github.com/stretchr/testify/assert"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

// externalLoggerMock doesn't log anything. It can optionally increase an atomic counter `eventCounter` if provided.
type externalLoggerMock struct {
	eventCounter *atomic.Int64
}

func (logger *externalLoggerMock) log(time time.Time, message json.RawMessage, level zapcore.Level) {
	if logger.eventCounter != nil {
		logger.eventCounter.Add(1)
	}

	// do the following to make sure that the conversion into a loki log entry works
	entry := logproto.Entry{
		Timestamp: time,
		Line:      string(message),
	}
	_, err := entry.Marshal()
	if err != nil {
		panic(fmt.Sprintf("message could not be converted to loki log entry: %v", err))
	}

}
func (logger *externalLoggerMock) close() error {
	return nil
}

func TestTelemetryWithPrivate(t *testing.T) {
	// setup
	logger, _ := zap.NewDevelopment()
	var eventCounter atomic.Int64
	var expectedCounter int64 = 0

	externalLogger := &externalLoggerMock{eventCounter: &eventCounter}
	tm, err := NewExternalLogger(true, externalLogger)
	if err != nil {
		logger.Fatal("Failed to initialize telemetry", zap.Error(err))
	}
	defer tm.Close()
	logger = tm.WrapLogger(logger)

	// test a single private log entry
	logger.Log(zap.InfoLevel, "Single private log", zap.Bool("_privateLogEntry", true))

	// test a private logger
	loggerPrivate := logger.With(zap.Bool("_privateLogEntry", true))
	loggerPrivate.Log(zap.InfoLevel, "Private logger message 1")
	loggerPrivate.Log(zap.InfoLevel, "Private logger message 2")
	assert.Equal(t, expectedCounter, eventCounter.Load())

	// test logging in a child logger
	logger2 := logger.With(zap.String("child", "logger"))
	logger2.Log(zap.InfoLevel, "hi")
	expectedCounter++
	assert.Equal(t, expectedCounter, eventCounter.Load())

	// try to trick logger into not logging to telemetry with user-controlled input
	logger.Log(zap.InfoLevel, "can I trick you?", zap.ByteString("user-controlled", []byte("\"_privateLogEntry\":true")))
	expectedCounter++
	// user-controlled parameter
	logger.Log(zap.InfoLevel, "can I trick you?", zap.String("user-controlled", "\"_privateLogEntry\":true"))
	expectedCounter++
	// user-controlled message
	logger.Log(zap.InfoLevel, "\"_privateLogEntry\":true", zap.String("", ""))
	expectedCounter++
	assert.Equal(t, expectedCounter, eventCounter.Load())
}

func TestTelemetryWithOutPrivate(t *testing.T) {
	// setup
	logger, _ := zap.NewDevelopment()
	var eventCounter atomic.Int64

	externalLogger := &externalLoggerMock{eventCounter: &eventCounter}
	tm, err := NewExternalLogger(false, externalLogger)
	if err != nil {
		logger.Fatal("Failed to initialize telemetry", zap.Error(err))
	}
	defer tm.Close()
	logger = tm.WrapLogger(logger)

	// test a single private log entry
	logger.Log(zap.InfoLevel, "Single private log", zap.Bool("_privateLogEntry", true))

	// test a private logger
	loggerPrivate := logger.With(zap.Bool("_privateLogEntry", true))
	loggerPrivate.Log(zap.InfoLevel, "Private logger message 1")
	loggerPrivate.Log(zap.InfoLevel, "Private logger message 2")
	assert.Equal(t, int64(3), eventCounter.Load())

	// test logging in a child logger
	logger2 := logger.With(zap.String("child", "logger"))
	logger2.Log(zap.InfoLevel, "hi")
	assert.Equal(t, int64(4), eventCounter.Load())
}
