package httpmock

import (
	"bytes"
	"encoding/json"
	"io"
	"net/http"
	"net/url"
	"os"
	"regexp"
	"strings"

	"github.com/cli/go-gh/v2/pkg/api"
)

type Matcher func(req *http.Request) bool
type Responder func(req *http.Request) (*http.Response, error)

type Stub struct {
	Stack     string
	matched   bool
	Matcher   Matcher
	Responder Responder
	exclude   bool
}

func MatchAny(*http.Request) bool {
	return true
}

// REST returns a matcher to a request for the HTTP method and URL escaped path p.
// For example, to match a GET request to `/api/v3/repos/octocat/hello-world/`
// use REST("GET", "api/v3/repos/octocat/hello-world")
// To match a GET request to `/user` use REST("GET", "user")
func REST(method, p string) Matcher {
	return func(req *http.Request) bool {
		if !strings.EqualFold(req.Method, method) {
			return false
		}
		return req.URL.EscapedPath() == "/"+p
	}
}

func GraphQL(q string) Matcher {
	re := regexp.MustCompile(q)

	return func(req *http.Request) bool {
		if !strings.EqualFold(req.Method, "POST") {
			return false
		}
		if req.URL.Path != "/graphql" && req.URL.Path != "/api/graphql" {
			return false
		}

		var bodyData struct {
			Query string
		}
		_ = decodeJSONBody(req, &bodyData)

		return re.MatchString(bodyData.Query)
	}
}

func GraphQLMutationMatcher(q string, cb func(map[string]interface{}) bool) Matcher {
	re := regexp.MustCompile(q)

	return func(req *http.Request) bool {
		if !strings.EqualFold(req.Method, "POST") {
			return false
		}
		if req.URL.Path != "/graphql" && req.URL.Path != "/api/graphql" {
			return false
		}

		var bodyData struct {
			Query     string
			Variables struct {
				Input map[string]interface{}
			}
		}
		_ = decodeJSONBody(req, &bodyData)

		if re.MatchString(bodyData.Query) {
			return cb(bodyData.Variables.Input)
		}

		return false
	}
}

func QueryMatcher(method string, path string, query url.Values) Matcher {
	return func(req *http.Request) bool {
		if !REST(method, path)(req) {
			return false
		}

		actualQuery := req.URL.Query()

		for param := range query {
			if !(actualQuery.Get(param) == query.Get(param)) {
				return false
			}
		}

		return true
	}
}

func readBody(req *http.Request) ([]byte, error) {
	bodyCopy := &bytes.Buffer{}
	r := io.TeeReader(req.Body, bodyCopy)
	req.Body = io.NopCloser(bodyCopy)
	return io.ReadAll(r)
}

func decodeJSONBody(req *http.Request, dest interface{}) error {
	b, err := readBody(req)
	if err != nil {
		return err
	}
	return json.Unmarshal(b, dest)
}

func StringResponse(body string) Responder {
	return func(req *http.Request) (*http.Response, error) {
		return httpResponse(200, req, bytes.NewBufferString(body)), nil
	}
}

func BinaryResponse(body []byte) Responder {
	return func(req *http.Request) (*http.Response, error) {
		return httpResponse(200, req, bytes.NewBuffer(body)), nil
	}
}

func WithHost(matcher Matcher, host string) Matcher {
	return func(req *http.Request) bool {
		if !strings.EqualFold(req.Host, host) {
			return false
		}
		return matcher(req)
	}
}

func WithHeader(responder Responder, header string, value string) Responder {
	return func(req *http.Request) (*http.Response, error) {
		resp, _ := responder(req)
		if resp.Header == nil {
			resp.Header = make(http.Header)
		}
		resp.Header.Set(header, value)
		return resp, nil
	}
}

func StatusStringResponse(status int, body string) Responder {
	return func(req *http.Request) (*http.Response, error) {
		return httpResponse(status, req, bytes.NewBufferString(body)), nil
	}
}

func JSONResponse(body interface{}) Responder {
	return func(req *http.Request) (*http.Response, error) {
		b, _ := json.Marshal(body)
		header := http.Header{
			"Content-Type": []string{"application/json"},
		}
		return httpResponseWithHeader(200, req, bytes.NewBuffer(b), header), nil
	}
}

// StatusJSONResponse turns the given argument into a JSON response.
//
// The argument is not meant to be a JSON string, unless it's intentional.
func StatusJSONResponse(status int, body interface{}) Responder {
	return func(req *http.Request) (*http.Response, error) {
		b, _ := json.Marshal(body)
		header := http.Header{
			"Content-Type": []string{"application/json"},
		}
		return httpResponseWithHeader(status, req, bytes.NewBuffer(b), header), nil
	}
}

// JSONErrorResponse is a type-safe helper to avoid confusion around the
// provided argument.
func JSONErrorResponse(status int, err api.HTTPError) Responder {
	return StatusJSONResponse(status, err)
}

func FileResponse(filename string) Responder {
	return func(req *http.Request) (*http.Response, error) {
		f, err := os.Open(filename)
		if err != nil {
			return nil, err
		}
		return httpResponse(200, req, f), nil
	}
}

func RESTPayload(responseStatus int, responseBody string, cb func(payload map[string]interface{})) Responder {
	return func(req *http.Request) (*http.Response, error) {
		bodyData := make(map[string]interface{})
		err := decodeJSONBody(req, &bodyData)
		if err != nil {
			return nil, err
		}
		cb(bodyData)

		header := http.Header{
			"Content-Type": []string{"application/json"},
		}
		return httpResponseWithHeader(responseStatus, req, bytes.NewBufferString(responseBody), header), nil
	}
}

func GraphQLMutation(body string, cb func(map[string]interface{})) Responder {
	return func(req *http.Request) (*http.Response, error) {
		var bodyData struct {
			Variables struct {
				Input map[string]interface{}
			}
		}
		err := decodeJSONBody(req, &bodyData)
		if err != nil {
			return nil, err
		}
		cb(bodyData.Variables.Input)

		return httpResponse(200, req, bytes.NewBufferString(body)), nil
	}
}

func GraphQLQuery(body string, cb func(string, map[string]interface{})) Responder {
	return func(req *http.Request) (*http.Response, error) {
		var bodyData struct {
			Query     string
			Variables map[string]interface{}
		}
		err := decodeJSONBody(req, &bodyData)
		if err != nil {
			return nil, err
		}
		cb(bodyData.Query, bodyData.Variables)

		return httpResponse(200, req, bytes.NewBufferString(body)), nil
	}
}

// ScopesResponder returns a response with a 200 status code and the given OAuth scopes.
func ScopesResponder(scopes string) func(*http.Request) (*http.Response, error) {
	//nolint:bodyclose
	return StatusScopesResponder(http.StatusOK, scopes)
}

// StatusScopesResponder returns a response with the given status code and OAuth scopes.
func StatusScopesResponder(status int, scopes string) func(*http.Request) (*http.Response, error) {
	return func(req *http.Request) (*http.Response, error) {
		return &http.Response{
			StatusCode: status,
			Request:    req,
			Header: map[string][]string{
				"X-Oauth-Scopes": {scopes},
			},
			Body: io.NopCloser(bytes.NewBufferString("")),
		}, nil
	}
}

func httpResponse(status int, req *http.Request, body io.Reader) *http.Response {
	return httpResponseWithHeader(status, req, body, http.Header{})
}

func httpResponseWithHeader(status int, req *http.Request, body io.Reader, header http.Header) *http.Response {
	return &http.Response{
		StatusCode: status,
		Request:    req,
		Body:       io.NopCloser(body),
		Header:     header,
	}
}
