package heimdall

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

	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/common/network"
	"github.com/ethereum/go-ethereum/consensus/bor/heimdall/checkpoint"

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

// HttpHandlerFake defines the handler functions required to serve
// requests to the mock heimdal server for specific functions. Add more handlers
// according to requirements.
type HttpHandlerFake struct {
	handleFetchCheckpoint http.HandlerFunc
	handleFetchMilestone  http.HandlerFunc
}

func (h *HttpHandlerFake) GetCheckpointHandler() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		h.handleFetchCheckpoint.ServeHTTP(w, r)
	}
}

func (h *HttpHandlerFake) GetMilestoneHandler() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		h.handleFetchMilestone.ServeHTTP(w, r)
	}
}

func CreateMockHeimdallServer(wg *sync.WaitGroup, port int, listener net.Listener, handler *HttpHandlerFake) (*http.Server, error) {
	// Create a new server mux
	mux := http.NewServeMux()

	// Create a route for fetching latest checkpoint
	mux.HandleFunc("/checkpoints/latest", func(w http.ResponseWriter, r *http.Request) {
		handler.GetCheckpointHandler()(w, r)
	})

	// Create a route for fetching milestone
	mux.HandleFunc("/milestones/latest", func(w http.ResponseWriter, r *http.Request) {
		handler.GetMilestoneHandler()(w, r)
	})

	// Add other routes as per requirement

	// Create the server with given port and mux
	srv := &http.Server{
		Addr:    fmt.Sprintf("localhost:%d", port),
		Handler: mux,
	}

	go func() {
		defer wg.Done()

		// Use the provided listener instead of closing it and creating a new one
		if err := srv.Serve(listener); err != http.ErrServerClosed {
			fmt.Printf("error in server.Serve(): %v", err)
		}
	}()

	return srv, nil
}

// TestFetchCheckpointFromMockHeimdall tests the heimdall client side logic
// to fetch checkpoints (latest for the scope of test) from a mock heimdall server.
// It can be used for debugging purpose (like response fields, marshalling/unmarshalling, etc).
func TestFetchCheckpointFromMockHeimdall(t *testing.T) {
	t.Parallel()

	// Create a wait group for sending across the mock server
	wg := &sync.WaitGroup{}
	wg.Add(1)

	// Initialize the fake handler and add a fake checkpoint handler function
	type gRPCGatewayCheckpointV2 struct {
		Proposer   common.Address `json:"proposer"`
		StartBlock string         `json:"start_block"`
		EndBlock   string         `json:"end_block"`
		RootHash   string         `json:"root_hash"`
		BorChainID string         `json:"bor_chain_id"`
		Timestamp  string         `json:"timestamp"`
	}

	type gRPCGatewayCheckpointResponseV2 struct {
		Result gRPCGatewayCheckpointV2 `json:"checkpoint"`
	}

	handler := &HttpHandlerFake{}
	handler.handleFetchCheckpoint = func(w http.ResponseWriter, _ *http.Request) {
		err := json.NewEncoder(w).Encode(gRPCGatewayCheckpointResponseV2{
			Result: gRPCGatewayCheckpointV2{
				Proposer:   common.Address{},
				StartBlock: "0",
				EndBlock:   "512",
				RootHash:   "dGVzdA==",
				BorChainID: "15001",
				Timestamp:  "0",
			},
		})

		if err != nil {
			w.WriteHeader(500) // Return 500 Internal Server Error.
		}
	}

	// Fetch available port
	port, listener, err := network.FindAvailablePort()
	require.NoError(t, err, "expect no error in finding available port")

	// Create mock heimdall server and pass handler instance for setting up the routes
	srv, err := CreateMockHeimdallServer(wg, port, listener, handler)
	require.NoError(t, err, "expect no error in starting mock heimdall server")

	// Add a small delay to ensure server is ready
	time.Sleep(100 * time.Millisecond)

	// Create a new heimdall client and use same port for connection
	client := NewHeimdallClient(fmt.Sprintf("http://localhost:%d", port), 5*time.Second)
	_, err = client.FetchCheckpoint(t.Context(), -1)
	require.NoError(t, err, "expect no error in fetching checkpoint")

	// Shutdown the server
	err = srv.Shutdown(t.Context())
	require.NoError(t, err, "expect no error in shutting down mock heimdall server")

	// Wait for `wg.Done()` to be called in the mock server's routine.
	wg.Wait()
}

// TestFetchMilestoneFromMockHeimdall tests the heimdall client side logic
// to fetch milestone from a mock heimdall server.
// It can be used for debugging purpose (like response fields, marshalling/unmarshalling, etc).
func TestFetchMilestoneFromMockHeimdall(t *testing.T) {
	t.Parallel()

	// Create a wait group for sending across the mock server
	wg := &sync.WaitGroup{}
	wg.Add(1)

	// Initialize the fake handler and add a fake milestone handler function
	type gRPCGatewayMilestoneV2 struct {
		Proposer        common.Address `json:"proposer"`
		StartBlock      string         `json:"start_block"`
		EndBlock        string         `json:"end_block"`
		Hash            string         `json:"hash"`
		BorChainID      string         `json:"bor_chain_id"`
		MilestoneID     string         `json:"milestone_id"`
		Timestamp       string         `json:"timestamp"`
		TotalDifficulty string         `json:"total_difficulty"`
	}

	type gRPCGatewayMilestoneResponseV2 struct {
		Result gRPCGatewayMilestoneV2 `json:"milestone"`
	}

	handler := &HttpHandlerFake{}
	handler.handleFetchMilestone = func(w http.ResponseWriter, _ *http.Request) {
		err := json.NewEncoder(w).Encode(gRPCGatewayMilestoneResponseV2{
			Result: gRPCGatewayMilestoneV2{
				Proposer:        common.Address{},
				StartBlock:      "0",
				EndBlock:        "512",
				Hash:            "dGVzdA==",
				BorChainID:      "15001",
				Timestamp:       "0",
				TotalDifficulty: "0",
			},
		})

		if err != nil {
			w.WriteHeader(500) // Return 500 Internal Server Error.
		}
	}

	// Fetch available port
	port, listener, err := network.FindAvailablePort()
	require.NoError(t, err, "expect no error in finding available port")

	// Create mock heimdall server and pass handler instance for setting up the routes
	srv, err := CreateMockHeimdallServer(wg, port, listener, handler)
	require.NoError(t, err, "expect no error in starting mock heimdall server")

	// Add a small delay to ensure server is ready
	time.Sleep(100 * time.Millisecond)

	// Create a new heimdall client and use same port for connection
	client := NewHeimdallClient(fmt.Sprintf("http://localhost:%d", port), 5*time.Second)
	_, err = client.FetchMilestone(t.Context())
	require.NoError(t, err, "expect no error in fetching milestone")

	// Shutdown the server
	err = srv.Shutdown(t.Context())
	require.NoError(t, err, "expect no error in shutting down mock heimdall server")

	// Wait for `wg.Done()` to be called in the mock server's routine.
	wg.Wait()
}

// TestFetchShutdown tests the heimdall client side logic for context timeout and
// interrupt handling while fetching data from a mock heimdall server.
func TestFetchShutdown(t *testing.T) {
	t.Parallel()

	// Create a wait group for sending across the mock server
	wg := &sync.WaitGroup{}
	wg.Add(1)

	// Initialize the fake handler and add a fake checkpoint handler function
	handler := &HttpHandlerFake{}

	// Case1 - Testing context timeout: Create delay in serving requests for simulating timeout. Add delay slightly
	// greater than `retryDelay`. This should cause the request to timeout and trigger shutdown
	// due to `ctx.Done()`. Expect context timeout error.
	handler.handleFetchCheckpoint = func(w http.ResponseWriter, _ *http.Request) {
		time.Sleep(100 * time.Millisecond)

		err := json.NewEncoder(w).Encode(checkpoint.CheckpointResponse{
			Result: checkpoint.Checkpoint{
				Proposer:   common.Address{},
				StartBlock: 0,
				EndBlock:   512,
				RootHash:   common.Hash{},
				BorChainID: "15001",
				Timestamp:  0,
			},
		})

		if err != nil {
			w.WriteHeader(500) // Return 500 Internal Server Error.
		}
	}

	// Fetch available port
	port, listener, err := network.FindAvailablePort()
	require.NoError(t, err, "expect no error in finding available port")

	// Create mock heimdall server and pass handler instance for setting up the routes
	srv, err := CreateMockHeimdallServer(wg, port, listener, handler)
	require.NoError(t, err, "expect no error in starting mock heimdall server")

	// Create a new heimdall client and use same port for connection
	client := NewHeimdallClient(fmt.Sprintf("http://localhost:%d", port), 5*time.Second)

	ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond)

	// Expect this to fail due to timeout
	_, err = client.FetchCheckpoint(ctx, -1)
	require.Equal(t, "context deadline exceeded", err.Error(), "expect the function error to be a context deadline exceeded error")
	require.Equal(t, "context deadline exceeded", ctx.Err().Error(), "expect the ctx error to be a context deadline exceeded error")

	cancel()

	// Case2 - Testing context cancellation. Pass a context with timeout to the request and
	// cancel it before timeout. This should cause the request to timeout and trigger shutdown
	// due to `ctx.Done()`. Expect context cancellation error.
	handler.handleFetchCheckpoint = func(w http.ResponseWriter, _ *http.Request) {
		time.Sleep(10 * time.Millisecond)
		w.WriteHeader(500) // Return 500 Internal Server Error.
	}

	ctx, cancel = context.WithTimeout(t.Context(), 50*time.Millisecond) // Use some high value for timeout

	// Cancel the context after a delay until we make request
	go func(cancel context.CancelFunc) {
		time.Sleep(10 * time.Millisecond)
		cancel()
	}(cancel)

	// Expect this to fail due to cancellation
	_, err = client.FetchCheckpoint(ctx, -1)
	require.Equal(t, "context canceled", err.Error(), "expect the function error to be a context cancelled error")
	require.Equal(t, "context canceled", ctx.Err().Error(), "expect the ctx error to be a context cancelled error")

	// Case3 - Testing interrupt: Closing the `closeCh` in heimdall client simulating interrupt. This
	// should cause the request to fail and throw an error due to `<-closeCh` in fetchWithRetry.
	// Expect shutdown detected error.
	handler.handleFetchCheckpoint = func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(500) // Return 500 Internal Server Error.
	}

	// Close the channel after a delay until we make request
	go func() {
		time.Sleep(1 * time.Second)
		close(client.closeCh)
	}()

	// Expect this to fail due to shutdown
	_, err = client.FetchCheckpoint(t.Context(), -1)
	require.Equal(t, ErrShutdownDetected.Error(), err.Error(), "expect the function error to be a shutdown detected error")

	// Shutdown the server
	err = srv.Shutdown(t.Context())
	require.NoError(t, err, "expect no error in shutting down mock heimdall server")

	// Wait for `wg.Done()` to be called in the mock server's routine.
	wg.Wait()
}

// TestContext includes bunch of simple tests to verify the working of timeout
// based context and cancellation.
func TestContext(t *testing.T) {
	t.Parallel()

	ctx, cancel1 := context.WithTimeout(t.Context(), 1*time.Second)

	// Case1: Done is not yet closed, so Err returns nil.
	require.NoError(t, ctx.Err(), "expect nil error")

	wg := &sync.WaitGroup{}

	// Case2: Check if timeout is being handled
	wg.Add(1)

	go func(ctx context.Context, wg *sync.WaitGroup) {
		defer wg.Done()
		select {
		case <-ctx.Done():
			// Expect context deadline exceeded error
			require.Equal(t, "context deadline exceeded", ctx.Err().Error(), "expect the ctx error to be a context deadline exceeded error")
		case <-time.After(2 * time.Second):
			// Case for safely exiting the tests
			return
		}
	}(ctx, wg)

	// Case3: Check normal case
	ctx, cancel2 := context.WithTimeout(t.Context(), 3*time.Second)

	wg.Add(1)

	errCh := make(chan error, 1)

	go func(ctx context.Context, wg *sync.WaitGroup) {
		defer wg.Done()
		select {
		case <-ctx.Done():
			// Expect this to never occur, throw explicit error
			errCh <- errors.New("unexpected call to `ctx.Done()`")
		case <-time.After(2 * time.Second):
			// Case for safely exiting the tests
			errCh <- nil
			return
		}
	}(ctx, wg)

	if err := <-errCh; err != nil {
		t.Fatalf("err: %v", err)
	}

	// Case4: Check if cancellation is being handled
	ctx, cancel3 := context.WithTimeout(t.Context(), 1*time.Second)

	wg.Add(1)

	go func(cancel context.CancelFunc) {
		time.Sleep(500 * time.Millisecond)
		cancel()
	}(cancel3)

	go func(ctx context.Context, wg *sync.WaitGroup) {
		defer wg.Done()
		select {
		case <-ctx.Done():
			// Expect context canceled error
			require.Equal(t, "context canceled", ctx.Err().Error(), "expect the ctx error to be a context canceled error")
		case <-time.After(2 * time.Second):
			// Case for safely exiting the tests
			return
		}
	}(ctx, wg)

	// Wait for all tests to pass
	wg.Wait()

	// Cancel all remaining contexts
	cancel1()
	cancel2()
}

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

	url, err := spanURL("http://bor0", 1)
	if err != nil {
		t.Fatal("got an error", err)
	}

	const expected = "http://bor0/bor/spans/1"

	if url.String() != expected {
		t.Fatalf("expected URL %q, got %q", expected, url.String())
	}
}

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

	url, err := stateSyncURL("http://bor0", 10, 100)
	if err != nil {
		t.Fatal("got an error", err)
	}

	const expected = "http://bor0/clerk/time?from_id=10&to_time=1970-01-01T00:01:40Z&pagination.limit=50"

	if url.String() != expected {
		t.Fatalf("expected URL %q, got %q", expected, url.String())
	}
}
