// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package encode_test

import (
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/caddyserver/caddy/v2"
	"github.com/caddyserver/caddy/v2/caddyconfig"
	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
	"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
	caddygzip "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode/gzip"
)

// recordingWriter records the moment WriteHeader reaches the underlying
// writer, so a test can distinguish "headers flushed to the client" from
// "headers still buffered inside the encoder".
type recordingWriter struct {
	http.ResponseWriter
	wroteHeader bool
	status      int
}

func (rw *recordingWriter) WriteHeader(status int) {
	if !rw.wroteHeader {
		rw.wroteHeader = true
		rw.status = status
	}
	rw.ResponseWriter.WriteHeader(status)
}

func (rw *recordingWriter) Flush() {
	if f, ok := rw.ResponseWriter.(http.Flusher); ok {
		f.Flush()
	}
}

func newSSEEncodeHandler(t *testing.T) *encode.Encode {
	t.Helper()
	enc := &encode.Encode{
		EncodingsRaw: caddy.ModuleMap{
			"gzip": caddyconfig.JSON(caddygzip.Gzip{}, nil),
		},
		Prefer: []string{"gzip"},
		// A large minimum_length means a normal small response would be
		// buffered (its header withheld) until enough bytes arrive; the SSE
		// path must bypass this so the handshake reaches the client.
		MinLength: 4096,
	}
	ctx, cancel := caddy.NewContext(caddy.Context{Context: t.Context()})
	t.Cleanup(cancel)
	if err := enc.Provision(ctx); err != nil {
		t.Fatalf("Provision() error = %v", err)
	}
	if err := enc.Validate(); err != nil {
		t.Fatalf("Validate() error = %v", err)
	}
	return enc
}

// An SSE upstream typically writes headers and flushes to establish the
// event stream before any event body is available. The encode middleware
// must let those headers reach the client immediately rather than holding
// them for minimum_length content sniffing. See #6293.
func TestSSEHeadersFlushedBeforeBody(t *testing.T) {
	enc := newSSEEncodeHandler(t)
	r := httptest.NewRequest(http.MethodGet, "/", nil)
	r.Header.Set("Accept-Encoding", "gzip")
	baseRec := httptest.NewRecorder()
	rec := &recordingWriter{ResponseWriter: baseRec}

	next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) error {
		w.Header().Set("Content-Type", "text/event-stream")
		w.WriteHeader(http.StatusOK)
		// Flush the way a real SSE handler does — through the response
		// controller (encode's writer implements FlushError, not Flush).
		if err := http.NewResponseController(w).Flush(); err != nil {
			t.Errorf("flush failed: %v", err)
		}
		// Before any event body is written, the client must already have the
		// headers AND the flush must have reached the underlying writer.
		if !rec.wroteHeader {
			t.Error("SSE response headers were not written to the client before the body")
		}
		if rec.status != http.StatusOK {
			t.Errorf("underlying status = %d, want 200", rec.status)
		}
		if !baseRec.Flushed {
			t.Error("underlying ResponseRecorder was not flushed for the SSE handshake")
		}
		return nil
	})

	if err := enc.ServeHTTP(rec, r, next); err != nil {
		t.Fatalf("ServeHTTP() error = %v", err)
	}
}

// A normal (non-SSE) small response is still allowed to buffer its header
// for content sniffing — the SSE change must not force every response to
// flush its header early. This guards the scope of the fix.
func TestNonSSESmallResponseStillBuffersHeader(t *testing.T) {
	enc := newSSEEncodeHandler(t)
	r := httptest.NewRequest(http.MethodGet, "/", nil)
	r.Header.Set("Accept-Encoding", "gzip")
	rec := &recordingWriter{ResponseWriter: httptest.NewRecorder()}

	next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) error {
		w.Header().Set("Content-Type", "text/plain")
		w.WriteHeader(http.StatusOK)
		if rec.wroteHeader {
			t.Error("non-SSE response flushed its header early; SSE bypass leaked to normal responses")
		}
		return nil
	})

	if err := enc.ServeHTTP(rec, r, next); err != nil {
		t.Fatalf("ServeHTTP() error = %v", err)
	}
}
