package processhttp

import (
	"context"
	"errors"
	"io"
	"net/http"
	"net/http/httptest"
	"sync"
	"testing"
	"time"
)

func TestProcessOwnsOperationalSurfaceDrainAndTransportCompletion(t *testing.T) {
	entered, release := make(chan struct{}), make(chan struct{})
	var once sync.Once
	product := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		once.Do(func() { close(entered) })
		<-release
		_, _ = io.WriteString(w, "ok")
	})
	process, err := NewProcess(ProcessConfig{
		Role: "example-appservice", Version: "sha", Address: "127.0.0.1:0", Handler: product,
		Capabilities: []string{"example.v1"}, Ready: func() bool { return true },
		Policy: ServerPolicy{ReadHeaderTimeout: time.Second, ReadTimeout: time.Second, WriteTimeout: time.Second, IdleTimeout: time.Second},
	})
	if err != nil {
		t.Fatal(err)
	}
	if process.Address() != "" {
		t.Fatal("constructor opened a listener")
	}
	if err := process.Wait(); err == nil {
		t.Fatal("wait before start did not fail")
	}
	if err := process.Shutdown(context.Background()); err == nil {
		t.Fatal("shutdown before start did not fail")
	}
	if err := process.Start(); err != nil {
		t.Fatal(err)
	}
	responseDone := make(chan error, 1)
	go func() {
		response, err := http.Get("http://" + process.Address() + "/product")
		if err == nil {
			_, _ = io.Copy(io.Discard, response.Body)
			err = response.Body.Close()
		}
		responseDone <- err
	}()
	select {
	case <-entered:
	case <-time.After(time.Second):
		t.Fatal("product request did not enter")
	}
	process.BeginDrain()
	response, err := http.Get("http://" + process.Address() + ReadyPath)
	if err != nil {
		t.Fatal(err)
	}
	_ = response.Body.Close()
	if response.StatusCode != http.StatusServiceUnavailable {
		t.Fatalf("ready status = %d", response.StatusCode)
	}
	shutdown := make(chan error, 1)
	go func() {
		ctx, cancel := context.WithTimeout(context.Background(), time.Second)
		defer cancel()
		shutdown <- process.Shutdown(ctx)
	}()
	select {
	case err := <-shutdown:
		t.Fatalf("shutdown returned before response completion: %v", err)
	case <-time.After(20 * time.Millisecond):
	}
	close(release)
	if err := <-responseDone; err != nil {
		t.Fatal(err)
	}
	if err := <-shutdown; err != nil {
		t.Fatal(err)
	}
	if err := process.Wait(); err != nil {
		t.Fatal(err)
	}
	if err := process.Wait(); err != nil {
		t.Fatalf("repeated wait = %v", err)
	}
	if err := process.Shutdown(context.Background()); err != nil {
		t.Fatalf("repeated shutdown = %v", err)
	}
}

func TestProcessPreservesFailedShutdownOutcome(t *testing.T) {
	entered := make(chan struct{})
	product := http.HandlerFunc(func(_ http.ResponseWriter, request *http.Request) {
		close(entered)
		<-request.Context().Done()
	})
	process, err := NewProcess(ProcessConfig{
		Role: "example-appservice", Version: "sha", Address: "127.0.0.1:0", Handler: product,
		Policy: ServerPolicy{ReadHeaderTimeout: time.Second, ReadTimeout: time.Second, WriteTimeout: time.Second, IdleTimeout: time.Second},
	})
	if err != nil {
		t.Fatal(err)
	}
	if err := process.Start(); err != nil {
		t.Fatal(err)
	}
	go func() {
		response, _ := http.Get("http://" + process.Address() + "/product")
		if response != nil {
			_ = response.Body.Close()
		}
	}()
	select {
	case <-entered:
	case <-time.After(time.Second):
		t.Fatal("product request did not enter")
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
	err = process.Shutdown(ctx)
	cancel()
	if !errors.Is(err, context.DeadlineExceeded) {
		t.Fatalf("shutdown error = %v", err)
	}
	if err := process.Shutdown(context.Background()); !errors.Is(err, context.DeadlineExceeded) {
		t.Fatalf("repeated shutdown error = %v", err)
	}
	if err := process.Wait(); err != nil {
		t.Fatalf("wait after forced close = %v", err)
	}
}

func TestProcessRejectsUnownedHijackedLifecycles(t *testing.T) {
	var calls int
	product := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		calls++
		if _, ok := w.(http.Hijacker); ok {
			t.Fatal("request/response process exposed http.Hijacker")
		}
		if _, _, err := http.NewResponseController(w).Hijack(); err == nil {
			t.Fatal("response controller bypassed the request/response lifecycle")
		}
		w.WriteHeader(http.StatusNoContent)
	})
	process, err := NewProcess(ProcessConfig{
		Role: "example-appservice", Version: "sha", Address: "127.0.0.1:0", Handler: product,
		Policy: ServerPolicy{ReadHeaderTimeout: time.Second, ReadTimeout: time.Second, WriteTimeout: time.Second, IdleTimeout: time.Second},
	})
	if err != nil {
		t.Fatal(err)
	}
	request := httptest.NewRequest(http.MethodGet, "/product", nil)
	request.Header.Set("Connection", "keep-alive, Upgrade")
	request.Header.Set("Upgrade", "websocket")
	response := httptest.NewRecorder()
	process.server.Handler.ServeHTTP(response, request)
	if response.Code != http.StatusBadRequest || calls != 0 {
		t.Fatalf("upgrade response = %d calls=%d", response.Code, calls)
	}

	plain := httptest.NewRecorder()
	process.server.Handler.ServeHTTP(plain, httptest.NewRequest(http.MethodGet, "/product", nil))
	if plain.Code != http.StatusNoContent || calls != 1 {
		t.Fatalf("plain response = %d calls=%d", plain.Code, calls)
	}
}
