package ldclient

import (
	"testing"
	"time"

	"github.com/launchdarkly/go-server-sdk/v6/internal/sharedtest/mocks"

	"github.com/launchdarkly/go-sdk-common/v3/ldlog"
	"github.com/launchdarkly/go-sdk-common/v3/ldlogtest"
	"github.com/launchdarkly/go-sdk-common/v3/lduser"
	"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
	ldevents "github.com/launchdarkly/go-sdk-events/v2"
	"github.com/launchdarkly/go-server-sdk/v6/interfaces"
	"github.com/launchdarkly/go-server-sdk/v6/ldcomponents"
	helpers "github.com/launchdarkly/go-test-helpers/v3"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestIdentifySendsIdentifyEvent(t *testing.T) {
	client := makeTestClient()
	defer client.Close()

	user := lduser.NewUser("userKey")
	err := client.Identify(user)
	assert.NoError(t, err)

	events := client.eventProcessor.(*mocks.CapturingEventProcessor).Events
	assert.Equal(t, 1, len(events))
	e := events[0].(ldevents.IdentifyEventData)
	assert.Equal(t, ldevents.Context(user), e.Context)
}

func TestIdentifyWithEmptyUserKeySendsNoEvent(t *testing.T) {
	client := makeTestClient()
	defer client.Close()

	err := client.Identify(lduser.NewUser(""))
	assert.NoError(t, err) // we don't return an error for this, we just log it

	events := client.eventProcessor.(*mocks.CapturingEventProcessor).Events
	assert.Equal(t, 0, len(events))
}

func TestTrackEventSendsCustomEvent(t *testing.T) {
	client := makeTestClient()
	defer client.Close()

	user := lduser.NewUser("userKey")
	key := "eventKey"
	err := client.TrackEvent(key, user)
	assert.NoError(t, err)

	events := client.eventProcessor.(*mocks.CapturingEventProcessor).Events
	assert.Equal(t, 1, len(events))
	e := events[0].(ldevents.CustomEventData)
	assert.Equal(t, ldevents.Context(user), e.Context)
	assert.Equal(t, key, e.Key)
	assert.Equal(t, ldvalue.Null(), e.Data)
	assert.False(t, e.HasMetric)
}

func TestTrackDataSendsCustomEventWithData(t *testing.T) {
	client := makeTestClient()
	defer client.Close()

	user := lduser.NewUser("userKey")
	key := "eventKey"
	data := ldvalue.ArrayOf(ldvalue.String("a"), ldvalue.String("b"))
	err := client.TrackData(key, user, data)
	assert.NoError(t, err)

	events := client.eventProcessor.(*mocks.CapturingEventProcessor).Events
	assert.Equal(t, 1, len(events))
	e := events[0].(ldevents.CustomEventData)
	assert.Equal(t, ldevents.Context(user), e.Context)
	assert.Equal(t, key, e.Key)
	assert.Equal(t, data, e.Data)
	assert.False(t, e.HasMetric)
}

func TestTrackMetricSendsCustomEventWithMetricAndData(t *testing.T) {
	client := makeTestClient()
	defer client.Close()

	user := lduser.NewUser("userKey")
	key := "eventKey"
	data := ldvalue.ArrayOf(ldvalue.String("a"), ldvalue.String("b"))
	metric := float64(1.5)
	err := client.TrackMetric(key, user, metric, data)
	assert.NoError(t, err)

	events := client.eventProcessor.(*mocks.CapturingEventProcessor).Events
	assert.Equal(t, 1, len(events))
	e := events[0].(ldevents.CustomEventData)
	assert.Equal(t, ldevents.Context(user), e.Context)
	assert.Equal(t, key, e.Key)
	assert.Equal(t, data, e.Data)
	assert.True(t, e.HasMetric)
	assert.Equal(t, metric, e.MetricValue)
}

func TestTrackWithEmptyUserKeySendsNoEvent(t *testing.T) {
	client := makeTestClient()
	defer client.Close()

	err := client.TrackEvent("eventkey", lduser.NewUser(""))
	assert.NoError(t, err) // we don't return an error for this, we just log it

	events := client.eventProcessor.(*mocks.CapturingEventProcessor).Events
	assert.Equal(t, 0, len(events))
}

func TestTrackMetricWithEmptyUserKeySendsNoEvent(t *testing.T) {
	client := makeTestClient()
	defer client.Close()

	err := client.TrackMetric("eventKey", lduser.NewUser(""), 2.5, ldvalue.Null())
	assert.NoError(t, err) // we don't return an error for this, we just log it

	events := client.eventProcessor.(*mocks.CapturingEventProcessor).Events
	assert.Equal(t, 0, len(events))
}

func TestIdentifyWithEventsDisabledDoesNotCauseError(t *testing.T) {
	mockLog := ldlogtest.NewMockLog()
	client := makeTestClientWithConfig(func(c *Config) {
		c.Events = ldcomponents.NoEvents()
		c.Logging = ldcomponents.Logging().Loggers(mockLog.Loggers)
	})
	defer client.Close()

	require.NoError(t, client.Identify(lduser.NewUser("")))

	assert.Len(t, mockLog.GetOutput(ldlog.Warn), 0)
}

func TestTrackWithEventsDisabledDoesNotCauseError(t *testing.T) {
	mockLog := ldlogtest.NewMockLog()
	client := makeTestClientWithConfig(func(c *Config) {
		c.Events = ldcomponents.NoEvents()
		c.Logging = ldcomponents.Logging().Loggers(mockLog.Loggers)
	})
	defer client.Close()

	require.NoError(t, client.TrackEvent("eventkey", lduser.NewUser("")))
	require.NoError(t, client.TrackMetric("eventkey", lduser.NewUser(""), 0, ldvalue.Null()))

	assert.Len(t, mockLog.GetOutput(ldlog.Warn), 0)
}

func TestWithEventsDisabledDecorator(t *testing.T) {
	doTest := func(name string, fn func(*LDClient) interfaces.LDClientInterface, shouldBeSent bool) {
		t.Run(name, func(t *testing.T) {
			events := &mocks.CapturingEventProcessor{}
			config := Config{
				DataSource: ldcomponents.ExternalUpdatesOnly(),
				Events:     mocks.SingleComponentConfigurer[ldevents.EventProcessor]{Instance: events},
			}
			client, err := MakeCustomClient("", config, 0)
			require.NoError(t, err)

			ci := fn(client)
			checkEvents := func(action func()) {
				events.Events = nil
				action()
				if shouldBeSent {
					assert.Len(t, events.Events, 1, "should have recorded an event, but did not")
				} else {
					assert.Len(t, events.Events, 0, "should not have recorded an event, but did")
				}
			}
			user := lduser.NewUser("userkey")
			checkEvents(func() { _, _ = ci.BoolVariation("flagkey", user, false) })
			checkEvents(func() { _, _, _ = ci.BoolVariationDetail("flagkey", user, false) })
			checkEvents(func() { _, _ = ci.IntVariation("flagkey", user, 0) })
			checkEvents(func() { _, _, _ = ci.IntVariationDetail("flagkey", user, 0) })
			checkEvents(func() { _, _ = ci.Float64Variation("flagkey", user, 0) })
			checkEvents(func() { _, _, _ = ci.Float64VariationDetail("flagkey", user, 0) })
			checkEvents(func() { _, _ = ci.StringVariation("flagkey", user, "") })
			checkEvents(func() { _, _, _ = ci.StringVariationDetail("flagkey", user, "") })
			checkEvents(func() { _, _ = ci.JSONVariation("flagkey", user, ldvalue.Null()) })
			checkEvents(func() { _, _, _ = ci.JSONVariationDetail("flagkey", user, ldvalue.Null()) })
			checkEvents(func() { ci.Identify(user) })
			checkEvents(func() { ci.TrackEvent("eventkey", user) })
			checkEvents(func() { ci.TrackData("eventkey", user, ldvalue.Bool(true)) })
			checkEvents(func() { ci.TrackMetric("eventkey", user, 1.5, ldvalue.Null()) })

			state := ci.AllFlagsState(user)
			assert.True(t, state.IsValid())
		})
	}

	doTest("client.WithEventsDisabled(false)",
		func(c *LDClient) interfaces.LDClientInterface { return c.WithEventsDisabled(false) },
		true)

	doTest("client.WithEventsDisabled(true)",
		func(c *LDClient) interfaces.LDClientInterface { return c.WithEventsDisabled(true) },
		false)

	doTest("client.WithEventsDisabled(true).WithEventsDisabled(false)",
		func(c *LDClient) interfaces.LDClientInterface {
			return c.WithEventsDisabled(true).WithEventsDisabled(false)
		},
		true)

	doTest("client.WithEventsDisabled(true).WithEventsDisabled(true)",
		func(c *LDClient) interfaces.LDClientInterface {
			return c.WithEventsDisabled(true).WithEventsDisabled(true)
		},
		false)
}

func TestFlushAsync(t *testing.T) {
	g := newGatedEventSender()
	client := makeTestClientWithEventSender(g)
	defer client.Close()

	client.Identify(evalTestUser)
	client.Flush()

	helpers.AssertNoMoreValues(t, g.didSendCh, time.Millisecond*50) // didn't do the flush yet

	g.canSendCh <- struct{}{} // allow the sender to proceed with the fake flush

	helpers.RequireValue(t, g.didSendCh, time.Millisecond*100) // now the flush has happened
}

func TestFlushAndWaitSucceeds(t *testing.T) {
	g := newGatedEventSender()
	client := makeTestClientWithEventSender(g)
	defer client.Close()

	client.Identify(evalTestUser)

	go func() {
		time.Sleep(time.Millisecond * 20)
		g.canSendCh <- struct{}{}
	}()

	sent := client.FlushAndWait(time.Millisecond * 500)
	assert.True(t, sent)

	helpers.RequireValue(t, g.didSendCh, time.Millisecond*50)
}

func TestFlushAndWaitTimesOut(t *testing.T) {
	g := newGatedEventSender()
	client := makeTestClientWithEventSender(g)
	defer client.Close()

	client.Identify(evalTestUser)

	go func() {
		time.Sleep(time.Millisecond * 200)
		g.canSendCh <- struct{}{}
	}()

	sent := client.FlushAndWait(time.Millisecond * 10)
	assert.False(t, sent)

	helpers.AssertNoMoreValues(t, g.didSendCh, time.Millisecond*50) // didn't do the flush yet
}

type gatedEventSender struct {
	canSendCh chan struct{}
	didSendCh chan struct{}
}

func newGatedEventSender() *gatedEventSender {
	return &gatedEventSender{canSendCh: make(chan struct{}, 100), didSendCh: make(chan struct{}, 100)}
}

func (g *gatedEventSender) SendEventData(kind ldevents.EventDataKind, data []byte, eventCount int) ldevents.EventSenderResult {
	<-g.canSendCh
	g.didSendCh <- struct{}{}
	return ldevents.EventSenderResult{Success: true}
}

func makeTestClientWithEventSender(s ldevents.EventSender) *LDClient {
	eventsConfig := ldevents.EventsConfiguration{
		Capacity:              1000,
		EventSender:           s,
		FlushInterval:         time.Hour,
		Loggers:               ldlog.NewDisabledLoggers(),
		UserKeysCapacity:      1000,
		UserKeysFlushInterval: time.Hour,
	}
	ep := ldevents.NewDefaultEventProcessor(eventsConfig)
	return makeTestClientWithConfig(func(c *Config) {
		c.Events = mocks.SingleComponentConfigurer[ldevents.EventProcessor]{Instance: ep}
	})
}
