// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package global

import (
	"context"
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"

	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/log"
	"go.opentelemetry.io/otel/log/embedded"
	"go.opentelemetry.io/otel/log/noop"
)

func TestLoggerProviderConcurrentSafe(*testing.T) {
	p := &loggerProvider{}

	done := make(chan struct{})
	stop := make(chan struct{})

	go func() {
		defer close(done)
		var logger log.Logger
		for i := 0; ; i++ {
			logger = p.Logger(fmt.Sprintf("a%d", i))
			select {
			case <-stop:
				_ = logger
				return
			default:
			}
		}
	}()

	p.setDelegate(noop.NewLoggerProvider())
	close(stop)
	<-done
}

func TestLoggerConcurrentSafe(t *testing.T) {
	l := &logger{}

	done := make(chan struct{})
	stop := make(chan struct{})

	go func() {
		defer close(done)

		ctx := t.Context()
		var r log.Record
		var param log.EnabledParameters

		var enabled bool
		for {
			l.Emit(ctx, r)
			enabled = l.Enabled(ctx, param)

			select {
			case <-stop:
				_ = enabled
				return
			default:
			}
		}
	}()

	l.setDelegate(noop.NewLoggerProvider())
	close(stop)
	<-done
}

type testLoggerProvider struct {
	embedded.LoggerProvider

	loggers map[string]*testLogger
	loggerN int
}

func (p *testLoggerProvider) Logger(name string, _ ...log.LoggerOption) log.Logger {
	if p.loggers == nil {
		l := &testLogger{}
		p.loggers = map[string]*testLogger{name: l}
		p.loggerN++
		return l
	}

	if l, ok := p.loggers[name]; ok {
		return l
	}

	p.loggerN++
	l := &testLogger{}
	p.loggers[name] = l
	return l
}

type testLogger struct {
	embedded.Logger

	emitN, enabledN int
}

func (l *testLogger) Emit(context.Context, log.Record) { l.emitN++ }
func (l *testLogger) Enabled(context.Context, log.EnabledParameters) bool {
	l.enabledN++
	return true
}

func emitRecord(l log.Logger) {
	ctx := context.Background()
	var param log.EnabledParameters
	var r log.Record

	_ = l.Enabled(ctx, param)
	l.Emit(ctx, r)
}

func TestDelegation(t *testing.T) {
	provider := &loggerProvider{}

	const preName = "pre"
	pre0, pre1 := provider.Logger(preName), provider.Logger(preName)
	assert.Same(t, pre0, pre1, "same logger instance not returned")

	alt := provider.Logger("alt")
	assert.NotSame(t, pre0, alt)

	alt2 := provider.Logger(preName, log.WithInstrumentationAttributes(attribute.String("k", "v")))
	assert.NotSame(t, pre0, alt2)

	delegate := &testLoggerProvider{}
	provider.setDelegate(delegate)

	want := 2 // (pre0/pre1) and (alt)
	if !assert.Equal(t, want, delegate.loggerN, "previous Loggers not delegated") {
		want = delegate.loggerN
	}

	pre2 := provider.Logger(preName)
	if !assert.Equal(t, want, delegate.loggerN, "previous Logger recreated") {
		want = delegate.loggerN
	}

	post := provider.Logger("test")
	want++
	assert.Equal(t, want, delegate.loggerN, "new Logger not delegated")

	emitRecord(pre0)
	emitRecord(pre2)

	if assert.IsType(t, &testLogger{}, pre2, "wrong pre-delegation Logger type") {
		assert.Equal(t, 2, pre2.(*testLogger).emitN, "Emit not delegated")
		assert.Equal(t, 2, pre2.(*testLogger).enabledN, "Enabled not delegated")
	}

	emitRecord(post)

	if assert.IsType(t, &testLogger{}, post, "wrong post-delegation Logger type") {
		assert.Equal(t, 1, post.(*testLogger).emitN, "Emit not delegated")
		assert.Equal(t, 1, post.(*testLogger).enabledN, "Enabled not delegated")
	}
}

func TestLoggerIdentity(t *testing.T) {
	type id struct{ name, ver, url string }

	ids := []id{
		{"name-a", "version-a", "url-a"},
		{"name-a", "version-a", "url-b"},
		{"name-a", "version-b", "url-a"},
		{"name-a", "version-b", "url-b"},
		{"name-b", "version-a", "url-a"},
		{"name-b", "version-a", "url-b"},
		{"name-b", "version-b", "url-a"},
		{"name-b", "version-b", "url-b"},
	}

	provider := &loggerProvider{}
	newLogger := func(i id) log.Logger {
		return provider.Logger(
			i.name,
			log.WithInstrumentationVersion(i.ver),
			log.WithSchemaURL(i.url),
		)
	}

	for i, id0 := range ids {
		for j, id1 := range ids {
			l0, l1 := newLogger(id0), newLogger(id1)

			if i == j {
				assert.Samef(t, l0, l1, "logger(%v) != logger(%v)", id0, id1)
			} else {
				assert.NotSamef(t, l0, l1, "logger(%v) == logger(%v)", id0, id1)
			}
		}
	}
}
