package event

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

	"github.com/go-logr/logr"
	larkcard "github.com/larksuite/oapi-sdk-go/v3/card"
	larkevent "github.com/larksuite/oapi-sdk-go/v3/event"
	larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
	"github.com/stretchr/testify/require"
)

func TestMemLoginDataStoreSaveAndConsume(t *testing.T) {
	t.Parallel()

	store := NewMemLoginDataStore()
	expected := LoginData{
		Source:    "workspace",
		Purpose:   "bind",
		UserEmail: "user@example.com",
		Extra:     json.RawMessage(`{"workspace_id":"workspace-1"}`),
	}

	id, err := store.Save(expected)
	require.NoError(t, err)
	require.NotEmpty(t, id)

	expected.ID = id
	actual, ok := store.Consume(id)
	require.True(t, ok)
	require.Equal(t, expected, actual)

	_, ok = store.Consume(id)
	require.False(t, ok)
}

func TestMemLoginDataStoreAlwaysGeneratesID(t *testing.T) {
	t.Parallel()

	store := NewMemLoginDataStore()
	data := LoginData{
		ID:        "login-data-1",
		Source:    "workspace",
		Purpose:   "bind",
		UserEmail: "user@example.com",
	}

	id, err := store.Save(data)
	require.NoError(t, err)
	require.NotEmpty(t, id)
	require.NotEqual(t, data.ID, id)

	actual, ok := store.Consume(id)
	require.True(t, ok)
	expected := data
	expected.ID = id
	require.Equal(t, expected, actual)
}

func TestLarkLoginDispatcherDispatchesRegisteredHandler(t *testing.T) {
	t.Parallel()

	store := NewMemLoginDataStore()
	dispatcher := NewLarkLoginDispatcher(logr.Discard(), store)
	expected := LoginData{
		Source:    "workspace",
		Purpose:   "bind",
		UserEmail: "user@example.com",
		Extra:     json.RawMessage(`{"workspace_id":"workspace-1"}`),
	}
	id, err := dispatcher.SaveLoginData(expected)
	require.NoError(t, err)
	expected.ID = id

	actualCh := make(chan struct {
		data            LoginData
		userAccessToken string
	}, 1)
	dispatcher.Register("workspace", func(ctx context.Context, data LoginData, userAccessToken string) error {
		actualCh <- struct {
			data            LoginData
			userAccessToken string
		}{data: data, userAccessToken: userAccessToken}
		return nil
	})

	require.NoError(t, dispatcher.Dispatch(context.Background(), id, "user-access-token"))
	actual := receiveValue(t, actualCh)
	require.Equal(t, expected, actual.data)
	require.Equal(t, "user-access-token", actual.userAccessToken)
}

func TestLarkLoginDispatcherGetsDataWithoutConsuming(t *testing.T) {
	t.Parallel()

	store := NewMemLoginDataStore()
	dispatcher := NewLarkLoginDispatcher(logr.Discard(), store)
	expected := LoginData{
		Source:    "workspace",
		Purpose:   "bind",
		UserEmail: "user@example.com",
		Extra:     json.RawMessage(`{"workspace_id":"workspace-1"}`),
	}
	id, err := dispatcher.SaveLoginData(expected)
	require.NoError(t, err)
	expected.ID = id

	actual, ok := dispatcher.GetLoginData(id)
	require.True(t, ok)
	require.Equal(t, expected, actual)

	actual, ok = dispatcher.GetLoginData(id)
	require.True(t, ok)
	require.Equal(t, expected, actual)
}

func TestLarkLoginDispatcherDispatchConsumesAfterGet(t *testing.T) {
	t.Parallel()

	store := NewMemLoginDataStore()
	dispatcher := NewLarkLoginDispatcher(logr.Discard(), store)
	id, err := dispatcher.SaveLoginData(LoginData{Source: "workspace"})
	require.NoError(t, err)

	_, ok := dispatcher.GetLoginData(id)
	require.True(t, ok)

	handled := make(chan struct{}, 1)
	dispatcher.Register("workspace", func(ctx context.Context, data LoginData, userAccessToken string) error {
		handled <- struct{}{}
		return nil
	})

	require.NoError(t, dispatcher.Dispatch(context.Background(), id, ""))
	receiveSignal(t, handled)

	_, ok = dispatcher.GetLoginData(id)
	require.False(t, ok)
}

func TestLarkLoginDispatcherConsumesDataOnce(t *testing.T) {
	t.Parallel()

	store := NewMemLoginDataStore()
	dispatcher := NewLarkLoginDispatcher(logr.Discard(), store)
	id, err := dispatcher.SaveLoginData(LoginData{Source: "workspace"})
	require.NoError(t, err)

	var handled atomic.Int64
	dispatcher.Register("workspace", func(ctx context.Context, data LoginData, userAccessToken string) error {
		handled.Add(1)
		return nil
	})

	require.NoError(t, dispatcher.Dispatch(context.Background(), id, ""))
	require.NoError(t, dispatcher.Dispatch(context.Background(), id, ""))
	require.Eventually(t, func() bool {
		return handled.Load() == 1
	}, time.Second, 10*time.Millisecond)
}

func TestLarkLoginDispatcherMissingIDReturnsNil(t *testing.T) {
	t.Parallel()

	dispatcher := NewLarkLoginDispatcher(logr.Discard(), NewMemLoginDataStore())

	require.NoError(t, dispatcher.Dispatch(context.Background(), "missing", ""))
}

func TestLarkLoginDispatcherUnregisteredSourceReturnsNil(t *testing.T) {
	t.Parallel()

	store := NewMemLoginDataStore()
	dispatcher := NewLarkLoginDispatcher(logr.Discard(), store)
	id, err := dispatcher.SaveLoginData(LoginData{Source: "workspace"})
	require.NoError(t, err)

	require.NoError(t, dispatcher.Dispatch(context.Background(), id, ""))
}

func TestLarkLoginDispatcherRunsHandlerErrorAsync(t *testing.T) {
	t.Parallel()

	store := NewMemLoginDataStore()
	dispatcher := NewLarkLoginDispatcher(logr.Discard(), store)
	id, err := dispatcher.SaveLoginData(LoginData{Source: "workspace"})
	require.NoError(t, err)

	expectedErr := errors.New("handle login failed")
	errCh := make(chan error, 1)
	dispatcher.Register("workspace", func(ctx context.Context, data LoginData, userAccessToken string) error {
		errCh <- expectedErr
		return expectedErr
	})

	require.NoError(t, dispatcher.Dispatch(context.Background(), id, ""))
	require.ErrorIs(t, receiveErr(t, errCh), expectedErr)
}

func TestLarkLoginDispatcherConcurrentRegisterSaveAndDispatch(t *testing.T) {
	t.Parallel()

	store := NewMemLoginDataStore()
	dispatcher := NewLarkLoginDispatcher(logr.Discard(), store)
	var handled atomic.Int64
	var wg sync.WaitGroup
	errCh := make(chan error, 64)

	for i := 0; i < 32; i++ {
		source := fmt.Sprintf("source-%d", i)

		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < 64; j++ {
				dispatcher.Register(source, func(ctx context.Context, data LoginData, userAccessToken string) error {
					handled.Add(1)
					return nil
				})
			}
		}()

		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < 64; j++ {
				id, err := dispatcher.SaveLoginData(LoginData{Source: source})
				if err != nil {
					errCh <- err
					return
				}
				if err := dispatcher.Dispatch(context.Background(), id, ""); err != nil {
					errCh <- err
					return
				}
			}
		}()
	}

	wg.Wait()
	close(errCh)
	for err := range errCh {
		require.NoError(t, err)
	}
}

func TestLarkCardDispatcherDispatchesRegisteredHandler(t *testing.T) {
	t.Parallel()

	dispatcher := NewLarkCardDispatcher("action", logr.Discard())
	resultCh := make(chan error, 1)
	dispatcher.Register("approve", func(ctx context.Context, value string, cardAction *larkcard.CardAction) error {
		if value != "approve" {
			return fmt.Errorf("dispatch value = %s, want approve", value)
		}
		resultCh <- nil
		return nil
	})

	start, err := dispatcher.Dispatch(context.Background(), newTestCardAction(t, "action", "approve"))
	require.NoError(t, err)
	require.NotNil(t, start)

	start()
	require.NoError(t, receiveErr(t, resultCh))
}

func TestLarkCardDispatcherConcurrentRegisterAndDispatch(t *testing.T) {
	t.Parallel()

	dispatcher := NewLarkCardDispatcher("action", logr.Discard())
	var handled atomic.Int64
	var wg sync.WaitGroup
	errCh := make(chan error, 64)

	for i := 0; i < 32; i++ {
		dispatchValue := fmt.Sprintf("value-%d", i)
		cardAction := newTestCardAction(t, "action", dispatchValue)

		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < 64; j++ {
				dispatcher.Register(dispatchValue, func(ctx context.Context, value string, cardAction *larkcard.CardAction) error {
					handled.Add(1)
					return nil
				})
			}
		}()

		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < 64; j++ {
				start, err := dispatcher.Dispatch(context.Background(), cardAction)
				if err != nil {
					errCh <- err
					return
				}
				start()
			}
		}()
	}

	wg.Wait()
	close(errCh)
	for err := range errCh {
		require.NoError(t, err)
	}
}

func TestLarkEventDispatcherDispatchesRegisteredHandler(t *testing.T) {
	t.Parallel()

	dispatcher := NewLarkEventDispatcher(logr.Discard())
	called := make(chan struct{}, 1)
	dispatcher.OnMessageReceive(func(ctx context.Context, event *larkim.P2MessageReceiveV1) error {
		called <- struct{}{}
		return nil
	})

	require.NoError(t, dispatcher.Dispatch(context.Background(), newTestMessageReceiveBody()))
	receiveSignal(t, called)
}

func TestLarkEventDispatcherConcurrentRegisterAndDispatch(t *testing.T) {
	t.Parallel()

	dispatcher := NewLarkEventDispatcher(logr.Discard())
	var handled atomic.Int64
	var wg sync.WaitGroup
	errCh := make(chan error, 64)

	for i := 0; i < 32; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < 32; j++ {
				dispatcher.OnMessageReceive(func(ctx context.Context, event *larkim.P2MessageReceiveV1) error {
					handled.Add(1)
					return nil
				})
			}
		}()

		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < 32; j++ {
				if err := dispatcher.Dispatch(context.Background(), newTestMessageReceiveBody()); err != nil {
					errCh <- err
					return
				}
			}
		}()
	}

	wg.Wait()
	close(errCh)
	for err := range errCh {
		require.NoError(t, err)
	}
}

func newTestCardAction(t *testing.T, dispatchKey string, dispatchValue string) *larkcard.CardAction {
	t.Helper()

	var cardAction larkcard.CardAction
	require.NoError(t, json.Unmarshal([]byte(fmt.Sprintf(`{"action":{"value":{%q:%q}}}`, dispatchKey, dispatchValue)), &cardAction))
	return &cardAction
}

func newTestMessageReceiveBody() *larkevent.EventV2Body {
	return &larkevent.EventV2Body{
		EventV2Base: larkevent.EventV2Base{
			Header: &larkevent.EventHeader{
				EventType: "im.message.receive_v1",
			},
		},
		Event: map[string]interface{}{},
	}
}

func receiveSignal(t *testing.T, ch <-chan struct{}) {
	t.Helper()

	select {
	case <-ch:
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for handler")
	}
}

func receiveErr(t *testing.T, ch <-chan error) error {
	t.Helper()

	select {
	case err := <-ch:
		return err
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for handler")
		return nil
	}
}

func receiveValue[T any](t *testing.T, ch <-chan T) T {
	t.Helper()

	select {
	case value := <-ch:
		return value
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for handler")
		var zero T
		return zero
	}
}
