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

package client

import (
	"bytes"
	"context"
	"encoding/json"
	"encoding/xml"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/cookiejar"
	"net/http/httptest"
	"net/url"
	"testing"
	"time"

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

const (
	token       = "the-super-secret-token"
	bearerToken = "Bearer " + token
	charsetUTF8 = ";charset=utf-8"
)

// task This describes a task. Tasks require a content property to be set.
type task struct {
	// Completed
	Completed bool `json:"completed" xml:"completed"`

	// Content Task content can contain [GFM](https://help.github.com/articles/github-flavored-markdown/).
	Content string `json:"content" xml:"content"`

	// ID This id property is autogenerated when a task is created.
	ID int64 `json:"id" xml:"id"`
}

type testCtxKey uint8

const rtKey testCtxKey = 1

func TestRuntime_Concurrent(t *testing.T) {
	// test that it can make a simple request
	// and get the response for it.
	// defaults all the way down
	result := []task{
		{false, taskOneContent, 1},
		{false, taskTwoContent, 2},
	}
	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
		rw.Header().Add(runtime.HeaderContentType, runtime.JSONMime)
		rw.WriteHeader(http.StatusOK)
		jsongen := json.NewEncoder(rw)
		assert.NoError(t, jsongen.Encode(result))
	}))
	defer server.Close()

	rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error {
		return nil
	})

	hu, err := url.Parse(server.URL)
	require.NoError(t, err)

	rt := New(hu.Host, "/", []string{schemeHTTP})
	resCC := make(chan any)
	errCC := make(chan error)
	var res any

	for range 6 {
		go func() {
			resC := make(chan any)
			errC := make(chan error)

			go func() {
				var resp any
				var errp error
				for range 3 {
					resp, errp = rt.Submit(&runtime.ClientOperation{
						ID:          operationID,
						Method:      http.MethodGet,
						PathPattern: "/",
						Params:      rwrtr,
						Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {
							if response.Code() == http.StatusOK {
								var res []task
								if e := consumer.Consume(response.Body(), &res); e != nil {
									return nil, e
								}
								return res, nil
							}
							return nil, errors.New("generic error")
						}),
					})
					<-time.After(100 * time.Millisecond)
				}
				resC <- resp
				errC <- errp
			}()
			resCC <- <-resC
			errCC <- <-errC
		}()
	}

	c := 6
	for c > 0 {
		res = <-resCC
		err = <-errCC
		c--
	}

	require.NoError(t, err)
	assert.IsType(t, []task{}, res)
	actual, ok := res.([]task)
	require.TrueT(t, ok)
	assert.Equal(t, result, actual)
}

func TestRuntime_Canary(t *testing.T) {
	// test that it can make a simple request
	// and get the response for it.
	// defaults all the way down
	result := []task{
		{false, taskOneContent, 1},
		{false, taskTwoContent, 2},
	}
	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
		rw.Header().Add(runtime.HeaderContentType, runtime.JSONMime)
		rw.WriteHeader(http.StatusOK)
		jsongen := json.NewEncoder(rw)
		assert.NoError(t, jsongen.Encode(result))
	}))
	defer server.Close()

	rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error {
		return nil
	})

	hu, err := url.Parse(server.URL)
	require.NoError(t, err)
	rt := New(hu.Host, "/", []string{schemeHTTP})
	res, err := rt.Submit(&runtime.ClientOperation{
		ID:          operationID,
		Method:      http.MethodGet,
		PathPattern: "/",
		Params:      rwrtr,
		Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {
			if response.Code() == http.StatusOK {
				var res []task
				if e := consumer.Consume(response.Body(), &res); e != nil {
					return nil, e
				}
				return res, nil
			}
			return nil, errors.New("generic error")
		}),
	})

	require.NoError(t, err)
	assert.IsType(t, []task{}, res)
	actual, ok := res.([]task)
	require.TrueT(t, ok)
	assert.Equal(t, result, actual)
}

type tasks struct {
	Tasks []task `xml:"task"`
}

func TestRuntime_XMLCanary(t *testing.T) {
	// test that it can make a simple XML request
	// and get the response for it.
	result := tasks{
		Tasks: []task{
			{false, taskOneContent, 1},
			{false, taskTwoContent, 2},
		},
	}
	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
		rw.Header().Add(runtime.HeaderContentType, runtime.XMLMime)
		rw.WriteHeader(http.StatusOK)
		xmlgen := xml.NewEncoder(rw)
		assert.NoError(t, xmlgen.Encode(result))
	}))
	defer server.Close()

	rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error {
		return nil
	})

	hu, err := url.Parse(server.URL)
	require.NoError(t, err)

	rt := New(hu.Host, "/", []string{schemeHTTP})
	res, err := rt.Submit(&runtime.ClientOperation{
		ID:          operationID,
		Method:      http.MethodGet,
		PathPattern: "/",
		Params:      rwrtr,
		Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {
			if response.Code() == http.StatusOK {
				var res tasks
				if e := consumer.Consume(response.Body(), &res); e != nil {
					return nil, e
				}
				return res, nil
			}
			return nil, errors.New("generic error")
		}),
	})

	require.NoError(t, err)
	assert.IsType(t, tasks{}, res)
	actual, ok := res.(tasks)
	require.TrueT(t, ok)
	assert.Equal(t, result, actual)
}

func TestRuntime_TextCanary(t *testing.T) {
	// test that it can make a simple text request
	// and get the response for it.
	result := "1: task 1 content; 2: task 2 content"
	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
		rw.Header().Add(runtime.HeaderContentType, runtime.TextMime)
		rw.WriteHeader(http.StatusOK)
		_, _ = rw.Write([]byte(result))
	}))
	defer server.Close()

	rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error {
		return nil
	})

	hu, err := url.Parse(server.URL)
	require.NoError(t, err)

	rt := New(hu.Host, "/", []string{schemeHTTP})
	res, err := rt.Submit(&runtime.ClientOperation{
		ID:          operationID,
		Method:      http.MethodGet,
		PathPattern: "/",
		Params:      rwrtr,
		Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {
			if response.Code() == http.StatusOK {
				var res string
				if e := consumer.Consume(response.Body(), &res); e != nil {
					return nil, e
				}
				return res, nil
			}
			return nil, errors.New("generic error")
		}),
	})

	require.NoError(t, err)
	assert.IsType(t, "", res)
	actual, ok := res.(string)
	require.TrueT(t, ok)
	assert.EqualT(t, result, actual)
}

func TestRuntime_CSVCanary(t *testing.T) {
	// test that it can make a simple csv request
	// and get the response for it.
	result := `task,content,result
1,task1,ok
2,task2,fail
`
	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
		rw.Header().Add(runtime.HeaderContentType, runtime.CSVMime)
		rw.WriteHeader(http.StatusOK)
		_, _ = rw.Write([]byte(result))
	}))
	defer server.Close()

	rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error {
		return nil
	})

	hu, err := url.Parse(server.URL)
	require.NoError(t, err)

	rt := New(hu.Host, "/", []string{schemeHTTP})
	res, err := rt.Submit(&runtime.ClientOperation{
		ID:          operationID,
		Method:      http.MethodGet,
		PathPattern: "/",
		Params:      rwrtr,
		Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {
			if response.Code() == http.StatusOK {
				var res bytes.Buffer
				if e := consumer.Consume(response.Body(), &res); e != nil {
					return nil, e
				}
				return res, nil
			}
			return nil, errors.New("generic error")
		}),
	})

	require.NoError(t, err)
	assert.IsType(t, bytes.Buffer{}, res)
	actual, ok := res.(bytes.Buffer)
	require.TrueT(t, ok)
	assert.EqualT(t, result, actual.String())
}

type roundTripperFunc func(*http.Request) (*http.Response, error)

func (fn roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
	return fn(req)
}

func TestRuntime_CustomTransport(t *testing.T) {
	rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error {
		return nil
	})
	result := []task{
		{false, taskOneContent, 1},
		{false, taskTwoContent, 2},
	}

	rt := New("localhost:3245", "/", []string{"ws", "wss", schemeHTTPS})
	rt.Transport = roundTripperFunc(func(req *http.Request) (*http.Response, error) {
		if req.URL.Scheme != schemeHTTPS {
			return nil, errors.New("this was not a https request")
		}
		assert.EqualT(t, "localhost:3245", req.Host)
		assert.EqualT(t, "localhost:3245", req.URL.Host)

		var resp http.Response
		resp.StatusCode = http.StatusOK
		resp.Header = make(http.Header)
		resp.Header.Set("Content-Type", "application/json")
		buf := bytes.NewBuffer(nil)
		enc := json.NewEncoder(buf)
		require.NoError(t, enc.Encode(result))
		resp.Body = io.NopCloser(buf)
		return &resp, nil
	})

	res, err := rt.Submit(&runtime.ClientOperation{
		ID:          operationID,
		Method:      http.MethodGet,
		PathPattern: "/",
		Schemes:     []string{"ws", "wss", schemeHTTPS},
		Params:      rwrtr,
		Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {
			if response.Code() == http.StatusOK {
				var res []task
				if e := consumer.Consume(response.Body(), &res); e != nil {
					return nil, e
				}
				return res, nil
			}
			return nil, errors.New("generic error")
		}),
	})

	require.NoError(t, err)
	assert.IsType(t, []task{}, res)
	actual, ok := res.([]task)
	require.TrueT(t, ok)
	assert.Equal(t, result, actual)
}

func TestRuntime_CustomCookieJar(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
		authenticated := false
		for _, cookie := range req.Cookies() {
			if cookie.Name == "sessionid" && cookie.Value == "abc" {
				authenticated = true
			}
		}
		if !authenticated {
			username, password, ok := req.BasicAuth()
			if ok && username == "username" && password == "password" {
				authenticated = true
				http.SetCookie(rw, &http.Cookie{ //nolint:gosec // it's okay for a local test cookie
					Name:  "sessionid",
					Value: "abc",
				})
			}
		}
		if authenticated {
			rw.Header().Add(runtime.HeaderContentType, runtime.JSONMime)
			rw.WriteHeader(http.StatusOK)
			jsongen := json.NewEncoder(rw)
			assert.NoError(t, jsongen.Encode([]task{}))
		} else {
			rw.WriteHeader(http.StatusUnauthorized)
		}
	}))
	defer server.Close()

	rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error {
		return nil
	})

	hu, err := url.Parse(server.URL)
	require.NoError(t, err)

	rt := New(hu.Host, "/", []string{schemeHTTP})
	rt.Jar, err = cookiejar.New(nil)
	require.NoError(t, err)

	submit := func(authInfo runtime.ClientAuthInfoWriter) {
		_, err := rt.Submit(&runtime.ClientOperation{
			ID:          operationID,
			Method:      http.MethodGet,
			PathPattern: "/",
			Params:      rwrtr,
			AuthInfo:    authInfo,
			Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, _ runtime.Consumer) (any, error) {
				if response.Code() == http.StatusOK {
					return map[string]any{}, nil
				}
				return nil, errors.New("generic error")
			}),
		})

		require.NoError(t, err)
	}

	submit(BasicAuth("username", "password"))
	submit(nil)
}

func TestRuntime_AuthCanary(t *testing.T) {

	// test that it can make a simple request
	// and get the response for it.
	// defaults all the way down
	result := []task{
		{false, taskOneContent, 1},
		{false, taskTwoContent, 2},
	}
	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
		if req.Header.Get(runtime.HeaderAuthorization) != bearerToken {
			rw.WriteHeader(http.StatusUnauthorized)
			return
		}

		rw.Header().Add(runtime.HeaderContentType, runtime.JSONMime)
		rw.WriteHeader(http.StatusOK)
		jsongen := json.NewEncoder(rw)
		assert.NoError(t, jsongen.Encode(result))
	}))
	defer server.Close()

	rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error {
		return nil
	})

	hu, err := url.Parse(server.URL)
	require.NoError(t, err)

	rt := New(hu.Host, "/", []string{schemeHTTP})
	res, err := rt.Submit(&runtime.ClientOperation{
		ID:     operationID,
		Params: rwrtr,
		Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {
			if response.Code() == http.StatusOK {
				var res []task
				if e := consumer.Consume(response.Body(), &res); e != nil {
					return nil, e
				}
				return res, nil
			}
			return nil, errors.New("generic error")
		}),
		AuthInfo: BearerToken(token),
	})

	require.NoError(t, err)
	assert.IsType(t, []task{}, res)
	actual, ok := res.([]task)
	require.TrueT(t, ok)
	assert.Equal(t, result, actual)
}

// TestPickConsumesMediaType covers the selection rule used by the client
// runtime. Two responsibilities:
//  1. multipart/form-data preference (issue #286);
//  2. producer-capability filter — skip consumes entries with no producer
//     registered, falling through to the next available match. Only fall
//     back to first-non-empty when nothing in consumes is registered (so
//     the gate at runtime.go:551 still emits its diagnostic).
func TestPickConsumesMediaType(t *testing.T) {
	const def = "application/json"
	defaultProducers := New("example.com", "/", []string{schemeHTTP}).Producers
	jsonOnly := map[string]runtime.Producer{
		runtime.JSONMime: runtime.JSONProducer(),
	}
	cases := []struct {
		name      string
		consumes  []string
		producers map[string]runtime.Producer
		want      string
	}{
		{"empty falls back to default", nil, defaultProducers, def},
		{"only empties fall back to default", []string{"", ""}, defaultProducers, def},
		{"single entry wins", []string{"text/plain"}, defaultProducers, "text/plain"},
		{"first non-empty wins when no multipart",
			[]string{"", runtime.URLencodedFormMime}, defaultProducers, runtime.URLencodedFormMime},
		{"multipart preferred when listed first",
			[]string{runtime.MultipartFormMime, runtime.URLencodedFormMime}, defaultProducers, runtime.MultipartFormMime},
		{"multipart preferred when listed last",
			[]string{runtime.URLencodedFormMime, runtime.MultipartFormMime}, defaultProducers, runtime.MultipartFormMime},
		{"caller's explicit single choice is honored",
			[]string{runtime.URLencodedFormMime}, defaultProducers, runtime.URLencodedFormMime},
		{"multipart match is case-insensitive",
			[]string{runtime.URLencodedFormMime, "Multipart/Form-Data"}, defaultProducers, "Multipart/Form-Data"},

		// Producer-capability filter (issues #32, #386).
		{"unregistered first entry falls through to registered second",
			[]string{"application/x-vendor", runtime.JSONMime}, jsonOnly, runtime.JSONMime},
		{"all unregistered entries: first non-empty preserved (gate fires)",
			[]string{vendorMime1, vendorMime2}, jsonOnly, vendorMime1},
		{"structural mime returns even without producer entry",
			[]string{runtime.URLencodedFormMime}, jsonOnly, runtime.URLencodedFormMime},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			assert.EqualT(t, tc.want, pickConsumesMediaType(tc.consumes, tc.producers, def))
		})
	}
}

func TestRuntime_PickConsumer(t *testing.T) {
	result := []task{
		{false, taskOneContent, 1},
		{false, taskTwoContent, 2},
	}
	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
		if req.Header.Get("Content-Type") != runtime.DefaultMime {
			rw.Header().Add(runtime.HeaderContentType, runtime.JSONMime+charsetUTF8)
			rw.WriteHeader(http.StatusBadRequest)
			return
		}
		rw.Header().Add(runtime.HeaderContentType, runtime.JSONMime+charsetUTF8)
		rw.WriteHeader(http.StatusOK)
		jsongen := json.NewEncoder(rw)
		_ = jsongen.Encode(result)
	}))
	defer server.Close()

	rwrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error {
		return req.SetBodyParam(bytes.NewBufferString("hello"))
	})

	hu, err := url.Parse(server.URL)
	require.NoError(t, err)

	rt := New(hu.Host, "/", []string{schemeHTTP})
	res, err := rt.Submit(&runtime.ClientOperation{
		ID:                 operationID,
		Method:             http.MethodPost,
		PathPattern:        "/",
		Schemes:            []string{schemeHTTP},
		ConsumesMediaTypes: []string{runtime.DefaultMime},
		Params:             rwrtr,
		Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {
			if response.Code() == http.StatusOK {
				var res []task
				if e := consumer.Consume(response.Body(), &res); e != nil {
					return nil, e
				}
				return res, nil
			}
			return nil, errors.New("generic error")
		}),
		AuthInfo: BearerToken(token),
	})

	require.NoError(t, err)
	assert.IsType(t, []task{}, res)
	actual, ok := res.([]task)
	require.TrueT(t, ok)
	assert.Equal(t, result, actual)
}

func TestRuntime_ContentTypeCanary(t *testing.T) {
	// test that it can make a simple request
	// and get the response for it.
	// defaults all the way down
	result := []task{
		{false, taskOneContent, 1},
		{false, taskTwoContent, 2},
	}
	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
		if req.Header.Get(runtime.HeaderAuthorization) != bearerToken {
			rw.WriteHeader(http.StatusBadRequest)
			return
		}
		rw.Header().Add(runtime.HeaderContentType, runtime.JSONMime+charsetUTF8)
		rw.WriteHeader(http.StatusOK)
		jsongen := json.NewEncoder(rw)
		_ = jsongen.Encode(result)
	}))
	defer server.Close()

	rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error {
		return nil
	})

	hu, err := url.Parse(server.URL)
	require.NoError(t, err)

	rt := New(hu.Host, "/", []string{schemeHTTP})
	res, err := rt.Submit(&runtime.ClientOperation{
		ID:          operationID,
		Method:      http.MethodGet,
		PathPattern: "/",
		Schemes:     []string{schemeHTTP},
		Params:      rwrtr,
		Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {
			if response.Code() == http.StatusOK {
				var res []task
				if e := consumer.Consume(response.Body(), &res); e != nil {
					return nil, e
				}
				return res, nil
			}
			return nil, errors.New("generic error")
		}),
		AuthInfo: BearerToken(token),
	})

	require.NoError(t, err)
	assert.IsType(t, []task{}, res)
	actual, ok := res.([]task)
	require.TrueT(t, ok)
	assert.Equal(t, result, actual)
}

func TestRuntime_ChunkedResponse(t *testing.T) {
	// test that it can make a simple request
	// and get the response for it.
	// defaults all the way down
	result := []task{
		{false, taskOneContent, 1},
		{false, taskTwoContent, 2},
	}
	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
		if req.Header.Get(runtime.HeaderAuthorization) != bearerToken {
			rw.WriteHeader(http.StatusBadRequest)
			return
		}
		rw.Header().Add(runtime.HeaderTransferEncoding, "chunked")
		rw.Header().Add(runtime.HeaderContentType, runtime.JSONMime+charsetUTF8)
		rw.WriteHeader(http.StatusOK)
		jsongen := json.NewEncoder(rw)
		_ = jsongen.Encode(result)
	}))
	defer server.Close()

	rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error {
		return nil
	})

	// specDoc, err := spec.Load("../../fixtures/codegen/todolist.simple.yml")
	hu, err := url.Parse(server.URL)
	require.NoError(t, err)

	rt := New(hu.Host, "/", []string{schemeHTTP})
	res, err := rt.Submit(&runtime.ClientOperation{
		ID:          operationID,
		Method:      http.MethodGet,
		PathPattern: "/",
		Schemes:     []string{schemeHTTP},
		Params:      rwrtr,
		Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {
			if response.Code() == http.StatusOK {
				var res []task
				if e := consumer.Consume(response.Body(), &res); e != nil {
					return nil, e
				}
				return res, nil
			}
			return nil, errors.New("generic error")
		}),
		AuthInfo: BearerToken(token),
	})

	require.NoError(t, err)
	assert.IsType(t, []task{}, res)
	actual, ok := res.([]task)
	require.TrueT(t, ok)
	assert.Equal(t, result, actual)
}

func TestRuntime_DebugValue(t *testing.T) {
	t.Run("empty DEBUG means Debug is False", func(t *testing.T) {
		t.Setenv("DEBUG", "")

		runtime := New("", "/", []string{schemeHTTPS})
		assert.FalseT(t, runtime.Debug)
	})

	t.Run("non-Empty DEBUG means Debug is True", func(t *testing.T) {
		t.Run("with numerical value", func(t *testing.T) {
			t.Setenv("DEBUG", "1")

			runtime := New("", "/", []string{schemeHTTPS})
			assert.TrueT(t, runtime.Debug)
		})

		t.Run("with boolean value true", func(t *testing.T) {
			t.Setenv("DEBUG", "true")

			runtime := New("", "/", []string{schemeHTTPS})
			assert.TrueT(t, runtime.Debug)
		})

		t.Run("with boolean value false", func(t *testing.T) {
			t.Setenv("DEBUG", "false")

			runtime := New("", "/", []string{schemeHTTPS})
			assert.FalseT(t, runtime.Debug)
		})

		t.Run("with string value ", func(t *testing.T) {
			t.Setenv("DEBUG", "foo")

			runtime := New("", "/", []string{schemeHTTPS})
			assert.TrueT(t, runtime.Debug)
		})
	})
}

func TestRuntime_OverrideScheme(t *testing.T) {
	runtime := New("", "/", []string{schemeHTTPS})
	sch := runtime.pickScheme([]string{schemeHTTP})
	assert.EqualT(t, schemeHTTPS, sch)
}

func TestRuntime_OverrideClient(t *testing.T) {
	client := &http.Client{}
	runtime := NewWithClient("", "/", []string{schemeHTTPS}, client)
	var i int
	runtime.clientOnce.Do(func() { i++ })
	assert.Equal(t, client, runtime.client)
	assert.EqualT(t, 0, i)
}

type overrideRoundTripper struct {
	overridden bool
}

func (o *overrideRoundTripper) RoundTrip(_ *http.Request) (*http.Response, error) {
	o.overridden = true
	res := new(http.Response)
	res.StatusCode = http.StatusOK
	res.Body = io.NopCloser(bytes.NewBufferString("OK"))
	return res, nil
}

func TestRuntime_OverrideClientOperation(t *testing.T) {
	client := &http.Client{}
	rt := NewWithClient("", "/", []string{schemeHTTPS}, client)
	var i int
	rt.clientOnce.Do(func() { i++ })
	assert.Equal(t, client, rt.client)
	assert.EqualT(t, 0, i)

	client2 := new(http.Client)
	var transport = &overrideRoundTripper{}
	client2.Transport = transport
	require.NotEqual(t, client, client2)

	_, err := rt.Submit(&runtime.ClientOperation{
		Client: client2,
		Params: runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error {
			return nil
		}),
		Reader: runtime.ClientResponseReaderFunc(func(_ runtime.ClientResponse, _ runtime.Consumer) (any, error) {
			return map[string]any{}, nil
		}),
	})
	require.NoError(t, err)
	assert.TrueT(t, transport.overridden)
}

func TestRuntime_PreserveTrailingSlash(t *testing.T) {
	var redirected bool

	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
		rw.Header().Add(runtime.HeaderContentType, runtime.JSONMime+charsetUTF8)

		if req.URL.Path == "/api/tasks" {
			redirected = true
			return
		}
		if req.URL.Path == "/api/tasks/" {
			rw.WriteHeader(http.StatusOK)
		}
	}))
	defer server.Close()

	hu, err := url.Parse(server.URL)
	require.NoError(t, err)

	rt := New(hu.Host, "/", []string{schemeHTTP})
	rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error {
		return nil
	})

	_, err = rt.Submit(&runtime.ClientOperation{
		ID:          operationID,
		Method:      http.MethodGet,
		PathPattern: "/api/tasks/",
		Params:      rwrtr,
		Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, _ runtime.Consumer) (any, error) {
			if redirected {
				return nil, errors.New("expected Submit to preserve trailing slashes - this caused a redirect")
			}
			if response.Code() == http.StatusOK {
				return map[string]any{}, nil
			}
			return nil, errors.New("generic error")
		}),
	})
	require.NoError(t, err)
}

func TestRuntime_FallbackConsumer(t *testing.T) {
	result := `W3siY29tcGxldGVkIjpmYWxzZSwiY29udGVudCI6ImRHRnpheUF4SUdOdmJuUmxiblE9IiwiaWQiOjF9XQ==`
	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
		rw.Header().Add(runtime.HeaderContentType, "application/x-task")
		rw.WriteHeader(http.StatusOK)
		_, _ = rw.Write([]byte(result))
	}))
	defer server.Close()

	rwrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error {
		return req.SetBodyParam(bytes.NewBufferString("hello"))
	})

	hu, err := url.Parse(server.URL)
	require.NoError(t, err)
	rt := New(hu.Host, "/", []string{schemeHTTP})

	// without the fallback consumer
	_, err = rt.Submit(&runtime.ClientOperation{
		ID:                 operationID,
		Method:             http.MethodPost,
		PathPattern:        "/",
		Schemes:            []string{schemeHTTP},
		ConsumesMediaTypes: []string{runtime.DefaultMime},
		Params:             rwrtr,
		Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {
			if response.Code() == http.StatusOK {
				var res []byte
				if e := consumer.Consume(response.Body(), &res); e != nil {
					return nil, e
				}
				return res, nil
			}
			return nil, errors.New("generic error")
		}),
	})
	require.Error(t, err)
	assert.EqualT(t, `no consumer: "application/x-task"`, err.Error())

	// add the fallback consumer
	rt.Consumers["*/*"] = rt.Consumers[runtime.DefaultMime]
	res, err := rt.Submit(&runtime.ClientOperation{
		ID:                 operationID,
		Method:             http.MethodPost,
		PathPattern:        "/",
		Schemes:            []string{schemeHTTP},
		ConsumesMediaTypes: []string{runtime.DefaultMime},
		Params:             rwrtr,
		Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {
			if response.Code() == http.StatusOK {
				var res []byte
				if e := consumer.Consume(response.Body(), &res); e != nil {
					return nil, e
				}
				return res, nil
			}
			return nil, errors.New("generic error")
		}),
	})

	require.NoError(t, err)
	assert.IsType(t, []byte{}, res)
	actual, ok := res.([]byte)
	require.TrueT(t, ok)
	assert.EqualValues(t, result, actual)
}

func TestRuntime_AuthHeaderParamDetected(t *testing.T) {
	// test that it can make a simple request
	// and get the response for it.
	// defaults all the way down
	result := []task{
		{false, taskOneContent, 1},
		{false, taskTwoContent, 2},
	}
	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
		if req.Header.Get(runtime.HeaderAuthorization) != bearerToken {
			rw.WriteHeader(http.StatusUnauthorized)
			return
		}
		rw.Header().Add(runtime.HeaderContentType, runtime.JSONMime)
		rw.WriteHeader(http.StatusOK)
		jsongen := json.NewEncoder(rw)
		_ = jsongen.Encode(result)
	}))
	defer server.Close()

	rwrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error {
		return req.SetHeaderParam(runtime.HeaderAuthorization, bearerToken)
	})

	hu, err := url.Parse(server.URL)
	require.NoError(t, err)

	rt := New(hu.Host, "/", []string{schemeHTTP})
	rt.DefaultAuthentication = BearerToken("not-the-super-secret-token")
	res, err := rt.Submit(&runtime.ClientOperation{
		ID:     operationID,
		Params: rwrtr,
		Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {
			if response.Code() == http.StatusOK {
				var res []task
				if e := consumer.Consume(response.Body(), &res); e != nil {
					return nil, e
				}
				return res, nil
			}
			return nil, errors.New("generic error")
		}),
	})

	require.NoError(t, err)
	assert.IsType(t, []task{}, res)
	actual, ok := res.([]task)
	require.TrueT(t, ok)
	assert.Equal(t, result, actual)
}

func TestRuntime_Timeout(t *testing.T) { //nolint:maintidx // linter evaluates the total lines of code, which is misleading
	const (
		// these values should be sufficient for most CI engines
		clientTimeout   time.Duration = 25 * time.Millisecond
		clientNoTimeout time.Duration = 250 * time.Millisecond
		ctxError                      = "context deadline exceeded"
	)
	result := []task{
		{false, taskOneContent, 1},
		{false, taskTwoContent, 2},
	}

	signedContext := func(value string) context.Context {
		return context.WithValue(context.Background(), rtKey, value)
	}

	requestWriter := func(timeout time.Duration) runtime.ClientRequestWriter {
		// this writer sets the timeout parameter of the ClientRequest
		return runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error {
			return req.SetTimeout(timeout)
		})
	}

	requestReader := runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {
		if response.Code() != http.StatusOK {
			return nil, errors.New("generic error")
		}

		var res []task
		if e := consumer.Consume(response.Body(), &res); e != nil {
			return nil, e
		}
		return res, nil
	})

	t.Run("with timeout specified as a request parameter, no operation context", func(t *testing.T) {
		host, cleaner := serverBuilder(t, result)
		t.Cleanup(cleaner)

		rt := New(host, "/", []string{schemeHTTP})
		rt.Context = signedContext("test")
		rt.Transport = testContextTransport(t, true, true, "test")

		t.Run("should not time out", func(t *testing.T) {
			res, err := rt.Submit(&runtime.ClientOperation{
				ID:      operationID,
				Context: nil,
				Params:  requestWriter(clientNoTimeout),
				Reader:  requestReader,
			})
			require.NoError(t, err)
			assertResult(result)(t, res)
		})

		t.Run("should time out", func(t *testing.T) {
			_, err := rt.Submit(&runtime.ClientOperation{
				ID:      operationID,
				Context: nil,
				Params:  requestWriter(clientTimeout),
				Reader:  requestReader,
			})
			require.Error(t, err)
			require.ErrorContains(t, err, ctxError)
		})
	})

	t.Run("with timeout specified as a request parameter, no context at all", func(t *testing.T) {
		host, cleaner := serverBuilder(t, result)
		t.Cleanup(cleaner)

		rt := New(host, "/", []string{schemeHTTP})
		rt.Context = nil
		rt.Transport = testContextTransport(t, true, false, "")

		t.Run("should not time out", func(t *testing.T) {
			res, err := rt.Submit(&runtime.ClientOperation{
				ID:      operationID,
				Context: nil,
				Params:  requestWriter(clientNoTimeout),
				Reader:  requestReader,
			})
			require.NoError(t, err)
			assertResult(result)(t, res)
		})

		t.Run("should time out", func(t *testing.T) {
			_, err := rt.Submit(&runtime.ClientOperation{
				ID:      operationID,
				Context: nil,
				Params:  requestWriter(clientTimeout),
				Reader:  requestReader,
			})
			require.Error(t, err)
			require.ErrorContains(t, err, ctxError)
		})
	})

	t.Run("with inherited operation context, timeout specified as operation context, request timeout set to 0", func(t *testing.T) {
		host, cleaner := serverBuilder(t, result)
		t.Cleanup(cleaner)

		rt := New(host, "/", []string{schemeHTTP})
		rt.Context = signedContext("test")
		rt.Transport = testContextTransport(t, true, true, "test")

		t.Run("should not time out", func(t *testing.T) {
			operationCtx, cancel := context.WithTimeout(rt.Context, clientNoTimeout)
			defer cancel()

			res, err := rt.Submit(&runtime.ClientOperation{
				ID:      operationID,
				Context: operationCtx,
				Params:  requestWriter(0),
				Reader:  requestReader,
			})
			require.NoError(t, err)
			assertResult(result)(t, res)
		})

		t.Run("should time out", func(t *testing.T) {
			operationCtx, cancel := context.WithTimeout(rt.Context, clientTimeout)
			defer cancel()

			_, err := rt.Submit(&runtime.ClientOperation{
				ID:      operationID,
				Context: operationCtx,
				Params:  requestWriter(0),
				Reader:  requestReader,
			})
			require.Error(t, err)
			require.ErrorContains(t, err, ctxError)
		})
	})

	t.Run("with a fresh operation context, timeout specified as operation context, request timeout set to 0", func(t *testing.T) {
		host, cleaner := serverBuilder(t, result)
		t.Cleanup(cleaner)
		rt := New(host, "/", []string{schemeHTTP})
		rt.Context = nil
		rt.Transport = testContextTransport(t, true, false, "")

		t.Run("should not time out", func(t *testing.T) {
			operationCtx, cancel := context.WithTimeout(context.Background(), clientNoTimeout)
			defer cancel()

			res, err := rt.Submit(&runtime.ClientOperation{
				ID:      operationID,
				Context: operationCtx,
				Params:  requestWriter(0),
				Reader:  requestReader,
			})
			require.NoError(t, err)
			assertResult(result)(t, res)
		})

		t.Run("should time out", func(t *testing.T) {
			operationCtx, cancel := context.WithTimeout(context.Background(), clientTimeout)
			defer cancel()

			_, err := rt.Submit(&runtime.ClientOperation{
				ID:      operationID,
				Context: operationCtx,
				Params:  requestWriter(0),
				Reader:  requestReader,
			})
			require.Error(t, err)
			require.ErrorContains(t, err, ctxError)
		})
	})

	t.Run("with an hypothetical timeout specified as runtime context, no operation context", func(t *testing.T) {
		// in real life, the runtime context may be cancellable for other reasons than timeout
		host, cleaner := serverBuilder(t, result)
		t.Cleanup(cleaner)

		t.Run("should not time out", func(t *testing.T) {
			rt := New(host, "/", []string{schemeHTTP})
			ctx, cancel := context.WithTimeout(signedContext("test"), clientNoTimeout)
			defer cancel()

			rt.Context = ctx
			rt.Transport = testContextTransport(t, true, true, "test")

			res, err := rt.Submit(&runtime.ClientOperation{
				ID:      operationID,
				Context: nil,
				Params:  requestWriter(0),
				Reader:  requestReader,
			})
			require.NoError(t, err)
			assertResult(result)(t, res)
		})

		t.Run("should time out", func(t *testing.T) {
			rt := New(host, "/", []string{schemeHTTP})
			ctx, cancel := context.WithTimeout(signedContext("test"), clientTimeout)
			defer cancel()

			rt.Context = ctx
			rt.Transport = testContextTransport(t, true, true, "test")

			_, err := rt.Submit(&runtime.ClientOperation{
				ID:      operationID,
				Context: nil,
				Params:  requestWriter(0),
				Reader:  requestReader,
			})
			require.Error(t, err)
			require.ErrorContains(t, err, ctxError)
		})
	})

	t.Run("with multiple timeouts set, shortest wins", func(t *testing.T) {
		host, cleaner := serverBuilder(t, result)
		t.Cleanup(cleaner)

		rt := New(host, "/", []string{schemeHTTP})
		runtimeCtx, cancelRuntime := context.WithTimeout(signedContext("test"), clientNoTimeout)
		rt.Context = runtimeCtx
		defer cancelRuntime()
		rt.Transport = testContextTransport(t, true, true, "test")

		t.Run("should not time out", func(t *testing.T) {
			operationCtx, cancelOperation := context.WithTimeout(
				signedContext("test"),
				serverDelay+(clientNoTimeout-serverDelay)/2,
			)
			defer cancelOperation()

			res, err := rt.Submit(&runtime.ClientOperation{
				ID:      operationID,
				Context: operationCtx,
				Params:  requestWriter(serverDelay + (clientNoTimeout-serverDelay)/3),
				Reader:  requestReader,
			})
			require.NoError(t, err)
			assertResult(result)(t, res)
		})

		t.Run("should time out on operation context deadline", func(t *testing.T) {
			// NOTE: we'll be able to catch more precisely which context was canceled
			// in go1.21 and context.WithTimeoutCause.
			operationCtx, cancelOperation := context.WithTimeout(
				signedContext("test"),
				serverDelay-(clientNoTimeout-serverDelay)/4, // this one times out
			)
			defer cancelOperation()

			_, err := rt.Submit(&runtime.ClientOperation{
				ID:      operationID,
				Context: operationCtx,
				Params: requestWriter(
					serverDelay + (clientNoTimeout-serverDelay)/4,
				),
				Reader: requestReader,
			})
			require.Error(t, err)
			require.ErrorContains(t, err, ctxError)
		})

		t.Run("should time out on operation timeout param", func(t *testing.T) {
			operationCtx, cancelOperation := context.WithTimeout(
				signedContext("test"),
				serverDelay+(clientNoTimeout-serverDelay)/2,
			)
			defer cancelOperation()

			_, err := rt.Submit(&runtime.ClientOperation{
				ID:      operationID,
				Context: operationCtx,
				Params: requestWriter(
					serverDelay - (clientNoTimeout-serverDelay)/4, // this one times out
				),
				Reader: requestReader,
			})
			require.Error(t, err)
			require.ErrorContains(t, err, ctxError)
		})
	})

	t.Run("with no context, explicit infinite wait", func(t *testing.T) {
		host, cleaner := serverBuilder(t, result)
		t.Cleanup(cleaner)

		rt := New(host, "/", []string{schemeHTTP})
		rt.Context = signedContext("test")
		rt.Transport = testContextTransport(t, false, true, "test") // verify that no deadline is passed to the emitted context

		t.Run("should not time out", func(t *testing.T) {
			resp, err := rt.Submit(&runtime.ClientOperation{
				ID:      operationID,
				Context: nil,
				Params:  requestWriter(0),
				Reader:  requestReader,
			})
			require.NoError(t, err)
			assertResult(result)(t, resp)
		})
	})
	t.Run("with no context, request uses the default timeout", func(t *testing.T) {
		requestEmptyWriter := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error {
			return nil
		})
		host, cleaner := serverBuilder(t, result)
		t.Cleanup(cleaner)

		rt := New(host, "/", []string{schemeHTTP})
		rt.Context = signedContext("test")
		rt.Transport = testDefaultsInTransport(t, "test")

		t.Run("should not time out", func(t *testing.T) {
			resp, err := rt.Submit(&runtime.ClientOperation{
				ID:      operationID,
				Context: nil,
				Params:  requestEmptyWriter, // leaves defaults
				Reader:  requestReader,
			})
			require.NoError(t, err)
			assertResult(result)(t, resp)
		})
	})
}

type testReqFn func(*testing.T, *http.Request)

type testRoundTripper struct {
	tr          http.RoundTripper
	testFn      testReqFn
	testHarness *testing.T
}

func (t *testRoundTripper) RoundTrip(req *http.Request) (resp *http.Response, err error) {
	t.testFn(t.testHarness, req)
	return t.tr.RoundTrip(req)
}

func TestGetBodyCallsBeforeRoundTrip(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
		rw.WriteHeader(http.StatusCreated)
		_, err := rw.Write([]byte("test result"))
		assert.NoError(t, err)
	}))
	defer server.Close()
	hu, err := url.Parse(server.URL)
	require.NoError(t, err)

	client := http.DefaultClient
	transport := http.DefaultTransport

	client.Transport = &testRoundTripper{
		tr:          transport,
		testHarness: t,
		testFn: func(t *testing.T, req *http.Request) {
			// Read the body once before sending the request
			body, e := req.GetBody()
			require.NoError(t, e)
			bodyContent, e := io.ReadAll(io.Reader(body))
			require.NoError(t, e)

			require.Len(t, bodyContent, int(req.ContentLength))
			require.EqualT(t, "\"test body\"\n", string(bodyContent))

			// Read the body a second time before sending the request
			body, e = req.GetBody()
			require.NoError(t, e)
			bodyContent, e = io.ReadAll(io.Reader(body))
			require.NoError(t, e)
			require.Len(t, bodyContent, int(req.ContentLength))
			require.EqualT(t, "\"test body\"\n", string(bodyContent))
		},
	}

	rwrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error {
		return req.SetBodyParam("test body")
	})

	operation := &runtime.ClientOperation{
		ID:          "getSites",
		Method:      http.MethodPost,
		PathPattern: "/",
		Params:      rwrtr,
		Client:      client,
		Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {
			if response.Code() == http.StatusCreated {
				var res string
				if e := consumer.Consume(response.Body(), &res); e != nil {
					return nil, e
				}
				return res, nil
			}
			return nil, errors.New("unexpected error code")
		}),
	}

	openAPIClient := New(hu.Host, "/", []string{schemeHTTP})
	res, err := openAPIClient.Submit(operation)
	require.NoError(t, err)

	actual, ok := res.(string)
	require.TrueT(t, ok)
	require.EqualT(t, "test result", actual)
}

func isContextSigned(ctx context.Context, value string) bool {
	v, ok := ctx.Value(rtKey).(string)

	return ok && v == value
}

func testContextTransport(t *testing.T, hasTimeout, expectSigned bool, value string) http.RoundTripper {

	// inject a round tripper to check the context in the request about to be emitted
	return roundTripperFunc(func(req *http.Request) (*http.Response, error) {
		ctx := req.Context()

		t.Run("request context should propagate value", func(t *testing.T) {
			assert.EqualT(t, expectSigned, isContextSigned(ctx, value), "expected the request context to inherit values")
		})

		t.Run(fmt.Sprintf("request context should have a deadline %t", hasTimeout), func(t *testing.T) {
			_, hasDeadline := ctx.Deadline()
			assert.EqualTf(t, hasTimeout, hasDeadline, "expected request context to have a deadline")
		})

		return http.DefaultTransport.RoundTrip(req)
	})
}

func testDefaultsInTransport(t *testing.T, value string) http.RoundTripper {
	// inject a round tripper to check the context in the request about to be emitted
	return roundTripperFunc(func(req *http.Request) (*http.Response, error) {
		ctx := req.Context()

		t.Run("request context should propagate value", func(t *testing.T) {
			assert.TrueT(t, isContextSigned(ctx, value), "expected the request context to inherit values")
		})

		t.Run("request context should have a default deadline", func(t *testing.T) {
			deadline, hasDeadline := ctx.Deadline()
			assert.TrueT(t, hasDeadline, "expected request context to have a deadline")

			remainingDuration := time.Until(deadline).Seconds()
			assert.InDeltaTf(t, DefaultTimeout.Seconds(), remainingDuration, 1.0, "expected timeout to be set to DefaultTimeout")
		})

		return http.DefaultTransport.RoundTrip(req)
	})
}

func assertResult(result []task) func(testing.TB, any) {
	return func(t testing.TB, res any) {
		assert.IsType(t, []task{}, res)
		actual, ok := res.([]task)
		require.TrueT(t, ok)
		assert.Equal(t, result, actual)
	}
}

const serverDelay = 100 * time.Millisecond

func serverBuilder(t testing.TB, result []task) (string, func()) {
	delay := serverDelay
	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
		ctx := req.Context()
		timer := time.NewTimer(delay)

		select {
		case <-ctx.Done():
			http.Error(rw, ctx.Err().Error(), http.StatusInternalServerError)

			return
		case <-timer.C:
			rw.Header().Add(runtime.HeaderContentType, runtime.JSONMime)
			rw.WriteHeader(http.StatusOK)
			jsongen := json.NewEncoder(rw)
			_ = jsongen.Encode(result)

			return
		}
	}))
	hu, err := url.Parse(server.URL)
	require.NoError(t, err)

	return hu.Host, server.Close
}
