// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package plog

import (
	"context"
	"io"
	"math"
	"strings"
	"testing"
	"time"

	"github.com/go-logr/logr"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"k8s.io/utils/clock"
	clocktesting "k8s.io/utils/clock/testing"
)

// contextKey type is unexported to prevent collisions.
type contextKey int

const zapOverridesKey contextKey = iota

func TestZapOverrides(ctx context.Context, t *testing.T, w io.Writer, f func(*zap.Config), opts ...zap.Option) context.Context {
	t.Helper() // discourage use outside of tests

	overrides := &testOverrides{
		t:    t,
		w:    w,
		f:    f,
		opts: opts,
	}
	return context.WithValue(ctx, zapOverridesKey, overrides)
}

func TestLogger(t *testing.T, w io.Writer) Logger {
	t.Helper()

	return New().withLogrMod(func(l logr.Logger) logr.Logger {
		return l.WithSink(TestZapr(t, w).GetSink())
	})
}

func TestZapr(t *testing.T, w io.Writer) logr.Logger {
	t.Helper()

	now, err := time.Parse(time.RFC3339Nano, "2099-08-08T13:57:36.123456789Z")
	require.NoError(t, err)

	ctx, cancel := context.WithCancel(context.Background())
	t.Cleanup(cancel)

	ctx = TestZapOverrides(ctx, t, w,
		func(config *zap.Config) {
			config.Level = zap.NewAtomicLevelAt(math.MinInt8) // log everything during tests

			// make test assertions less painful to write while keeping them as close to the real thing as possible
			config.EncoderConfig.EncodeCaller = func(caller zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) {
				trimmed := caller.TrimmedPath()
				if idx := strings.LastIndexByte(trimmed, ':'); idx != -1 {
					trimmed = trimmed[:idx+1] + "<line>"
				}
				enc.AppendString(trimmed + funcEncoder(caller))
			}
		},
		zap.WithClock(ZapClock(clocktesting.NewFakeClock(now))), // have the clock be static during tests
		zap.AddStacktrace(nopLevelEnabler{}),                    // do not log stacktraces
	)

	// there is no buffering so we can ignore flush
	zl, _, err := newLogr(ctx, "json", 0)
	require.NoError(t, err)

	return zl
}

var _ zapcore.Clock = &clockAdapter{}

type clockAdapter struct {
	clock clock.Clock
}

func (c *clockAdapter) Now() time.Time {
	return c.clock.Now()
}

func (c *clockAdapter) NewTicker(duration time.Duration) *time.Ticker {
	return &time.Ticker{C: c.clock.Tick(duration)}
}

func ZapClock(c clock.Clock) zapcore.Clock {
	return &clockAdapter{clock: c}
}

var _ zap.Sink = nopCloserSink{}

type nopCloserSink struct{ zapcore.WriteSyncer }

func (nopCloserSink) Close() error { return nil }

// newSink returns a wrapper around the input writer that is safe for concurrent use.
func newSink(w io.Writer) zap.Sink {
	return nopCloserSink{WriteSyncer: zapcore.Lock(zapcore.AddSync(w))}
}

var _ zapcore.LevelEnabler = nopLevelEnabler{}

type nopLevelEnabler struct{}

func (nopLevelEnabler) Enabled(_ zapcore.Level) bool { return false }

type testOverrides struct {
	t    *testing.T
	w    io.Writer
	f    func(*zap.Config)
	opts []zap.Option
}