// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0

package runtime

import (
	"bufio"
	"bytes"
	"context"
	"io"
	"net/http"
	"net/url"
	"strings"
	"testing"

	"github.com/go-openapi/testify/v2/require"

	"github.com/go-openapi/testify/v2/assert"
)

type eofReader struct{}

func (e *eofReader) Read(_ []byte) (int, error) {
	return 0, io.EOF
}

func closeReader(rdr io.Reader) *closeCounting {
	return &closeCounting{
		rdr: rdr,
	}
}

type closeCounting struct {
	rdr    io.Reader
	closed int
}

func (c *closeCounting) Read(d []byte) (int, error) {
	return c.rdr.Read(d)
}

func (c *closeCounting) Close() error {
	c.closed++
	if cr, ok := c.rdr.(io.ReadCloser); ok {
		return cr.Close()
	}
	return nil
}

type countingBufioReader struct {
	buffereds int
	peeks     int
	reads     int

	br interface {
		Buffered() int
		Peek(int) ([]byte, error)
		Read([]byte) (int, error)
	}
}

func (c *countingBufioReader) Buffered() int {
	c.buffereds++
	return c.br.Buffered()
}

func (c *countingBufioReader) Peek(v int) ([]byte, error) {
	c.peeks++
	return c.br.Peek(v)
}

func (c *countingBufioReader) Read(p []byte) (int, error) {
	c.reads++
	return c.br.Read(p)
}

func TestPeekingReader(t *testing.T) {
	// just passes to original reader when nothing called
	exp1 := []byte("original")
	pr1 := newPeekingReader(closeReader(bytes.NewReader(exp1)))
	b1, err := io.ReadAll(pr1)
	require.NoError(t, err)
	assert.Equal(t, exp1, b1)

	// uses actual when there was some buffering
	exp2 := []byte("actual")
	pr2 := newPeekingReader(closeReader(bytes.NewReader(exp2)))
	peeked, err := pr2.underlying.Peek(1)
	require.NoError(t, err)
	require.EqualT(t, "a", string(peeked))
	b2, err := io.ReadAll(pr2)
	require.NoError(t, err)
	assert.EqualT(t, string(exp2), string(b2))

	// passes close call through to original reader
	cr := closeReader(closeReader(bytes.NewReader(exp2)))
	pr3 := newPeekingReader(cr)
	require.NoError(t, pr3.Close())
	require.EqualT(t, 1, cr.closed)

	// returns false when the stream is empty
	pr4 := newPeekingReader(closeReader(&eofReader{}))
	require.FalseT(t, pr4.HasContent())

	// returns true when the stream has content
	rdr := closeReader(strings.NewReader("hello"))
	pr := newPeekingReader(rdr)
	cbr := &countingBufioReader{
		br: bufio.NewReader(rdr),
	}
	pr.underlying = cbr

	require.TrueT(t, pr.HasContent())
	require.EqualT(t, 1, cbr.buffereds)
	require.EqualT(t, 1, cbr.peeks)
	require.EqualT(t, 0, cbr.reads)
	require.TrueT(t, pr.HasContent())
	require.EqualT(t, 2, cbr.buffereds)
	require.EqualT(t, 1, cbr.peeks)
	require.EqualT(t, 0, cbr.reads)

	b, err := io.ReadAll(pr)
	require.NoError(t, err)
	require.EqualT(t, "hello", string(b))
	require.EqualT(t, 2, cbr.buffereds)
	require.EqualT(t, 1, cbr.peeks)
	require.EqualT(t, 2, cbr.reads)
	require.EqualT(t, 0, cbr.br.Buffered())

	t.Run("closing a closed peekingReader", func(t *testing.T) {
		const content = "content"
		r := newPeekingReader(io.NopCloser(strings.NewReader(content)))
		require.NoError(t, r.Close())

		require.NotPanics(t, func() {
			err := r.Close()
			require.Error(t, err)
		})
	})

	t.Run("reading from a closed peekingReader", func(t *testing.T) {
		const content = "content"
		r := newPeekingReader(io.NopCloser(strings.NewReader(content)))
		require.NoError(t, r.Close())

		require.NotPanics(t, func() {
			_, err := io.ReadAll(r)
			require.Error(t, err)
			require.ErrorIs(t, err, io.ErrUnexpectedEOF)
		})
	})

	t.Run("reading from a nil peekingReader", func(t *testing.T) {
		var r *peekingReader
		require.NotPanics(t, func() {
			buf := make([]byte, 10)
			_, err := r.Read(buf)
			require.Error(t, err)
			require.ErrorIs(t, err, io.EOF)
		})
	})
}

func TestJSONRequest(t *testing.T) {
	req, err := JSONRequest(http.MethodGet, "/swagger.json", nil)
	require.NoError(t, err)
	assert.EqualT(t, http.MethodGet, req.Method)
	assert.EqualT(t, JSONMime, req.Header.Get(HeaderContentType))
	assert.EqualT(t, JSONMime, req.Header.Get(HeaderAccept))

	req, err = JSONRequest(http.MethodGet, "%2", nil)
	require.Error(t, err)
	assert.Nil(t, req)
}

func TestHasBody(t *testing.T) {
	req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", nil)
	require.NoError(t, err)
	assert.FalseT(t, HasBody(req))

	req.ContentLength = 123
	assert.TrueT(t, HasBody(req))
}

func TestMethod(t *testing.T) {
	testcase := []struct {
		method      string
		canHaveBody bool
		allowsBody  bool
		isSafe      bool
	}{
		{"put", true, true, false},
		{"post", true, true, false},
		{"patch", true, true, false},
		{"delete", true, true, false},
		{"get", false, true, true},
		{"options", false, true, false},
		{"head", false, false, true},
		{"invalid", false, true, false},
		{"", false, true, false},
	}

	for _, tc := range testcase {
		t.Run(tc.method, func(t *testing.T) {
			assert.EqualT(t, tc.canHaveBody, CanHaveBody(tc.method), "CanHaveBody")

			req := http.Request{Method: tc.method}
			assert.EqualT(t, tc.allowsBody, AllowsBody(&req), "AllowsBody")
			assert.EqualT(t, tc.isSafe, IsSafe(&req), "IsSafe")
		})
	}
}

func TestReadSingle(t *testing.T) {
	values := url.Values(make(map[string][]string))
	values.Add("something", "the thing")
	assert.EqualT(t, "the thing", ReadSingleValue(Values(values), "something"))
	assert.Empty(t, ReadSingleValue(Values(values), "notthere"))
}

func TestReadCollection(t *testing.T) {
	values := url.Values(make(map[string][]string))
	values.Add("something", "value1,value2")
	assert.Equal(t, []string{"value1", "value2"}, ReadCollectionValue(Values(values), "something", "csv"))
	assert.Empty(t, ReadCollectionValue(Values(values), "notthere", ""))
}
