package main

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

func wait(t *testing.T, ch <-chan struct{}) {
	t.Helper()
	select {
	case <-ch:
	case <-time.After(3 * time.Second):
		t.Fatal("timed out waiting for observable completion")
	}
}

func TestCancellationReachesUpstream(t *testing.T) {
	started, stopped := make(chan struct{}), make(chan struct{})
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		close(started)
		<-r.Context().Done()
		close(stopped)
	}))
	defer upstream.Close()
	api := httptest.NewServer(handler(upstream.Client(), upstream.URL))
	defer api.Close()
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	result := make(chan error, 1)
	go func() { _, err := fetch(ctx, api.Client(), api.URL); result <- err }()
	wait(t, started)
	cancel()
	select {
	case err := <-result:
		if !errors.Is(err, context.Canceled) {
			t.Fatalf("want context.Canceled, got %v", err)
		}
	case <-time.After(3 * time.Second):
		t.Fatal("HTTP client did not return after cancellation")
	}
	wait(t, stopped) // Verify the work exits, not merely that the caller stopped waiting.
}

func TestHandlerReturnsBody(t *testing.T) {
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = io.WriteString(w, "hello")
	}))
	defer upstream.Close()
	response := httptest.NewRecorder()
	handler(upstream.Client(), upstream.URL)(response, httptest.NewRequest(http.MethodGet, "/", nil))
	if response.Code != http.StatusOK || response.Body.String() != "hello" {
		t.Fatalf("unexpected response: %d %q", response.Code, response.Body.String())
	}
}

func TestSelectEvaluatesSendValueBeforeChoosing(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	called := false
	var ch chan string // Nil: the send cannot win.
	select {
	case ch <- func() string { called = true; return "value" }():
		t.Fatal("send on nil channel must not be selected")
	case <-ctx.Done():
	}
	if !called {
		t.Fatal("send value must be evaluated even though cancellation wins")
	}
}

func TestCanceledSendHasAnExitPath(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	result := make(chan string) // No receiver.
	done := make(chan struct{})
	go func() {
		defer close(done)
		select {
		case result <- "already computed":
		case <-ctx.Done():
			return
		}
	}()
	cancel()
	wait(t, done)
}
