// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package raft_test

import (
	"context"
	"errors"
	"math/rand"
	"net"
	"sync/atomic"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"

	"github.com/hashicorp/consul/internal/storage"
	"github.com/hashicorp/consul/internal/storage/conformance"
	"github.com/hashicorp/consul/internal/storage/raft"
	"github.com/hashicorp/consul/sdk/testutil"
)

func TestBackend_Conformance(t *testing.T) {
	t.Run("Leader", func(t *testing.T) {
		conformance.Test(t, conformance.TestOptions{
			NewBackend: func(t *testing.T) storage.Backend {
				leader, _ := newRaftCluster(t)
				return leader
			},
			SupportsStronglyConsistentList: true,
		})
	})

	t.Run("Follower", func(t *testing.T) {
		conformance.Test(t, conformance.TestOptions{
			NewBackend: func(t *testing.T) storage.Backend {
				_, follower := newRaftCluster(t)
				return follower
			},
			SupportsStronglyConsistentList: true,
		})
	})
}

func newRaftCluster(t *testing.T) (*raft.Backend, *raft.Backend) {
	t.Helper()

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

	lis, err := net.Listen("tcp", ":0")
	require.NoError(t, err)

	lc, err := grpc.DialContext(ctx, lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
	require.NoError(t, err)

	lh := &leaderHandle{replCh: make(chan log, 10)}
	leader, err := raft.NewBackend(lh, testutil.Logger(t))
	require.NoError(t, err)
	lh.backend = leader
	go leader.Run(ctx)

	go func() {
		for {
			conn, err := lis.Accept()
			if errors.Is(err, net.ErrClosed) {
				return
			}
			require.NoError(t, err)
			go leader.HandleConnection(conn)
		}
	}()

	follower, err := raft.NewBackend(&followerHandle{leaderConn: lc}, testutil.Logger(t))
	require.NoError(t, err)
	go follower.Run(ctx)
	follower.LeaderChanged()

	go lh.replicate(t, follower)

	return leader, follower
}

type followerHandle struct {
	leaderConn *grpc.ClientConn
}

func (followerHandle) Apply([]byte) (any, error) {
	return nil, errors.New("not leader")
}

func (followerHandle) IsLeader() bool {
	return false
}

func (followerHandle) EnsureStrongConsistency(context.Context) error {
	return errors.New("not leader")
}

func (f *followerHandle) DialLeader() (*grpc.ClientConn, error) {
	return f.leaderConn, nil
}

type leaderHandle struct {
	index  uint64
	replCh chan log

	backend *raft.Backend
}

type log struct {
	idx uint64
	msg []byte
}

func (l *leaderHandle) Apply(msg []byte) (any, error) {
	idx := atomic.AddUint64(&l.index, 1)

	// Apply the operation to the leader synchronously and capture its response
	// to return to the caller.
	rsp := l.backend.Apply(msg, idx)

	// Replicate the operation to the follower asynchronously.
	l.replCh <- log{idx, msg}

	if err, ok := rsp.(error); ok {
		return nil, err
	}
	return rsp, nil
}

func (leaderHandle) IsLeader() bool {
	return true
}

func (leaderHandle) EnsureStrongConsistency(context.Context) error {
	return nil
}

func (leaderHandle) DialLeader() (*grpc.ClientConn, error) {
	return nil, errors.New("leader should not dial itself")
}

func (h *leaderHandle) replicate(t *testing.T, follower *raft.Backend) {
	doneCh := make(chan struct{})
	t.Cleanup(func() { close(doneCh) })

	timer := time.NewTimer(replicationLag())
	defer timer.Stop()

	for {
		select {
		case <-timer.C:
			select {
			case l := <-h.replCh:
				_ = follower.Apply(l.msg, l.idx)
			default:
			}
			timer.Reset(replicationLag())
		case <-doneCh:
			return
		}
	}
}

func replicationLag() time.Duration {
	if testing.Short() {
		return 0
	}
	return time.Duration(rand.Intn(50)) * time.Millisecond
}
