package api

import (
	"context"
	"errors"
	"io"
	"net"
	"net/http"
	"testing"
	"time"

	"github.com/gofiber/fiber/v2"
)

func TestServerShutdownDrainsInFlightRequestsAndRejectsNewRequests(t *testing.T) {
	app := fiber.New(fiber.Config{
		DisableStartupMessage: true,
	})
	UseDefaultMiddlewares(app, testHTTPConfig())

	slowStarted := make(chan struct{})
	app.Get("/slow", func(c *fiber.Ctx) error {
		select {
		case <-slowStarted:
		default:
			close(slowStarted)
		}

		select {
		case <-time.After(300 * time.Millisecond):
		case <-c.UserContext().Done():
			return c.Status(http.StatusInternalServerError).SendString("user context canceled")
		}

		return c.SendString("slow done")
	})
	app.Get("/fast", func(c *fiber.Ctx) error {
		return c.SendString("fast done")
	})

	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen: %v", err)
	}

	serveErrCh := make(chan error, 1)
	go func() {
		serveErrCh <- app.Listener(ln)
	}()

	t.Cleanup(func() {
		_ = app.Shutdown()
	})

	baseURL := "http://" + ln.Addr().String()
	fastClient := newTestHTTPClient()
	slowClient := newTestHTTPClient()
	defer fastClient.CloseIdleConnections()
	defer slowClient.CloseIdleConnections()

	if err := waitForHTTP200(fastClient, baseURL+"/fast", time.Second); err != nil {
		t.Fatalf("wait for server ready: %v", err)
	}

	slowRespCh := make(chan httpCallResult, 1)
	go func() {
		resp, err := slowClient.Get(baseURL + "/slow")
		if err != nil {
			slowRespCh <- httpCallResult{err: err}
			return
		}
		defer resp.Body.Close()

		body, readErr := io.ReadAll(resp.Body)
		slowRespCh <- httpCallResult{
			statusCode: resp.StatusCode,
			body:       string(body),
			err:        readErr,
		}
	}()

	select {
	case <-slowStarted:
	case <-time.After(time.Second):
		t.Fatal("slow request did not start")
	}

	srv := NewServer(app, WithShutdownTimeout(2*time.Second))
	shutdownErrCh := make(chan error, 1)
	go func() {
		shutdownErrCh <- srv.Shutdown(context.Background())
	}()

	if err := waitForRequestRejection(fastClient, baseURL+"/fast", time.Second); err == nil {
		t.Fatal("expected new requests to be rejected during shutdown")
	}

	slowResult := <-slowRespCh
	if slowResult.err != nil {
		t.Fatalf("slow request failed: %v", slowResult.err)
	}
	if slowResult.statusCode != http.StatusOK {
		t.Fatalf("expected slow request 200, got %d with body %q", slowResult.statusCode, slowResult.body)
	}
	if slowResult.body != "slow done" {
		t.Fatalf("expected slow request body %q, got %q", "slow done", slowResult.body)
	}

	if err := <-shutdownErrCh; err != nil {
		t.Fatalf("shutdown failed: %v", err)
	}
	if err := <-serveErrCh; err != nil {
		t.Fatalf("listener exited with error: %v", err)
	}
}

type httpCallResult struct {
	statusCode int
	body       string
	err        error
}

func newTestHTTPClient() *http.Client {
	return &http.Client{
		Timeout: time.Second,
		Transport: &http.Transport{
			DisableKeepAlives: true,
		},
	}
}

func waitForHTTP200(client *http.Client, url string, timeout time.Duration) error {
	deadline := time.Now().Add(timeout)
	for time.Now().Before(deadline) {
		resp, err := client.Get(url)
		if err == nil {
			resp.Body.Close()
			if resp.StatusCode == http.StatusOK {
				return nil
			}
		}
		time.Sleep(10 * time.Millisecond)
	}
	return errors.New("server did not become ready")
}

func waitForRequestRejection(client *http.Client, url string, timeout time.Duration) error {
	deadline := time.Now().Add(timeout)
	var lastErr error
	for time.Now().Before(deadline) {
		resp, err := client.Get(url)
		if err != nil {
			return err
		}
		lastErr = errors.New("request still accepted")
		resp.Body.Close()
		time.Sleep(10 * time.Millisecond)
	}
	return lastErr
}
