package httpmock

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"runtime"
	"strings"
	"sync/atomic"

	"github.com/jarcoal/httpmock/internal"
)

var ignorePackages = map[string]bool{}

func init() {
	IgnoreMatcherHelper()
}

// IgnoreMatcherHelper should be called by external helpers building
// [Matcher], typically in an init() function, to avoid they appear in
// the autogenerated [Matcher] names.
func IgnoreMatcherHelper(skip ...int) {
	sk := 2
	if len(skip) > 0 {
		sk += skip[0]
	}
	if pkg := getPackage(sk); pkg != "" {
		ignorePackages[pkg] = true
	}
}

// Copied from github.com/maxatome/go-testdeep/internal/trace.getPackage.
func getPackage(skip int) string {
	if pc, _, _, ok := runtime.Caller(skip); ok {
		if fn := runtime.FuncForPC(pc); fn != nil {
			return extractPackage(fn.Name())
		}
	}
	return ""
}

// extractPackage extracts package part from a fully qualified function name:
//
//	"foo/bar/test.fn"            → "foo/bar/test"
//	"foo/bar/test.X.fn"          → "foo/bar/test"
//	"foo/bar/test.(*X).fn"       → "foo/bar/test"
//	"foo/bar/test.(*X).fn.func1" → "foo/bar/test"
//	"weird"                      → ""
//
// Derived from github.com/maxatome/go-testdeep/internal/trace.SplitPackageFunc.
func extractPackage(fn string) string {
	sp := strings.LastIndexByte(fn, '/')
	if sp < 0 {
		sp = 0 // std package
	}

	dp := strings.IndexByte(fn[sp:], '.')
	if dp < 0 {
		return ""
	}

	return fn[:sp+dp]
}

// calledFrom returns a string like "@PKG.FUNC() FILE:LINE".
func calledFrom(skip int) string {
	pc := make([]uintptr, 128)
	npc := runtime.Callers(skip+1, pc)
	pc = pc[:npc]

	frames := runtime.CallersFrames(pc)

	var lastFrame runtime.Frame

	for {
		frame, more := frames.Next()

		// If testing package is encountered, it is too late
		if strings.HasPrefix(frame.Function, "testing.") {
			break
		}
		lastFrame = frame
		// Stop if httpmock is not the caller
		if !ignorePackages[extractPackage(frame.Function)] || !more {
			break
		}
	}

	if lastFrame.Line == 0 {
		return ""
	}
	return fmt.Sprintf(" @%s() %s:%d",
		lastFrame.Function, lastFrame.File, lastFrame.Line)
}

// MatcherFunc type is the function to use to check a [Matcher]
// matches an incoming request. When httpmock calls a function of this
// type, it is guaranteed req.Body is never nil. If req.Body is nil in
// the original request, it is temporarily replaced by an instance
// returning always [io.EOF] for each Read() call, during the call.
type MatcherFunc func(req *http.Request) bool

func matcherFuncOr(mfs []MatcherFunc) MatcherFunc {
	return func(req *http.Request) bool {
		for _, mf := range mfs {
			rearmBody(req)
			if mf(req) {
				return true
			}
		}
		return false
	}
}

func matcherFuncAnd(mfs []MatcherFunc) MatcherFunc {
	if len(mfs) == 0 {
		return nil
	}
	return func(req *http.Request) bool {
		for _, mf := range mfs {
			rearmBody(req)
			if !mf(req) {
				return false
			}
		}
		return true
	}
}

// Check returns true if mf is nil, otherwise it returns mf(req).
func (mf MatcherFunc) Check(req *http.Request) bool {
	return mf == nil || mf(req)
}

// Or combines mf and all mfs in a new [MatcherFunc]. This new
// [MatcherFunc] succeeds if one of mf or mfs succeeds. Note that as
// a nil [MatcherFunc] is considered succeeding, if mf or one of mfs
// items is nil, nil is returned.
func (mf MatcherFunc) Or(mfs ...MatcherFunc) MatcherFunc {
	if len(mfs) == 0 || mf == nil {
		return mf
	}
	cmfs := make([]MatcherFunc, len(mfs)+1)
	cmfs[0] = mf
	for i, cur := range mfs {
		if cur == nil {
			return nil
		}
		cmfs[i+1] = cur
	}
	return matcherFuncOr(cmfs)
}

// And combines mf and all mfs in a new [MatcherFunc]. This new
// [MatcherFunc] succeeds if all of mf and mfs succeed. Note that a
// [MatcherFunc] also succeeds if it is nil, so if mf and all mfs
// items are nil, nil is returned.
func (mf MatcherFunc) And(mfs ...MatcherFunc) MatcherFunc {
	if len(mfs) == 0 {
		return mf
	}
	cmfs := make([]MatcherFunc, 0, len(mfs)+1)
	if mf != nil {
		cmfs = append(cmfs, mf)
	}
	for _, cur := range mfs {
		if cur != nil {
			cmfs = append(cmfs, cur)
		}
	}
	return matcherFuncAnd(cmfs)
}

// Matcher type defines a match case. The zero Matcher{} corresponds
// to the default case. Otherwise, use [NewMatcher] or any helper
// building a [Matcher] like [BodyContainsBytes], [BodyContainsBytes],
// [HeaderExists], [HeaderIs], [HeaderContains] or any of
// [github.com/maxatome/tdhttpmock] functions.
type Matcher struct {
	name string
	fn   MatcherFunc // can be nil → means always true
}

var matcherID int64

// NewMatcher returns a [Matcher]. If name is empty and fn is non-nil,
// a name is automatically generated. When fn is nil, it is a default
// [Matcher]: its name can be empty.
//
// Automatically generated names have the form:
//
//	~HEXANUMBER@PKG.FUNC() FILE:LINE
//
// Legend:
//   - HEXANUMBER is a unique 10 digit hexadecimal number, always increasing;
//   - PKG is the NewMatcher caller package (except if
//     [IgnoreMatcherHelper] has been previously called, in this case it
//     is the caller of the caller package and so on);
//   - FUNC is the function name of the caller in the previous PKG package;
//   - FILE and LINE are the location of the call in FUNC function.
func NewMatcher(name string, fn MatcherFunc) Matcher {
	if name == "" && fn != nil {
		// Auto-name the matcher
		name = fmt.Sprintf("~%010x%s", atomic.AddInt64(&matcherID, 1), calledFrom(1))
	}
	return Matcher{
		name: name,
		fn:   fn,
	}
}

// BodyContainsBytes returns a [Matcher] checking that request body
// contains subslice.
//
// The name of the returned [Matcher] is auto-generated (see [NewMatcher]).
// To name it explicitly, use [Matcher.WithName] as in:
//
//	BodyContainsBytes([]byte("foo")).WithName("10-body-contains-foo")
//
// See also [github.com/maxatome/tdhttpmock.Body],
// [github.com/maxatome/tdhttpmock.JSONBody] and
// [github.com/maxatome/tdhttpmock.XMLBody] for powerful body testing.
func BodyContainsBytes(subslice []byte) Matcher {
	return NewMatcher("",
		func(req *http.Request) bool {
			rearmBody(req)
			b, err := io.ReadAll(req.Body)
			return err == nil && bytes.Contains(b, subslice)
		})
}

// BodyContainsString returns a [Matcher] checking that request body
// contains substr.
//
// The name of the returned [Matcher] is auto-generated (see [NewMatcher]).
// To name it explicitly, use [Matcher.WithName] as in:
//
//	BodyContainsString("foo").WithName("10-body-contains-foo")
//
// See also [github.com/maxatome/tdhttpmock.Body],
// [github.com/maxatome/tdhttpmock.JSONBody] and
// [github.com/maxatome/tdhttpmock.XMLBody] for powerful body testing.
func BodyContainsString(substr string) Matcher {
	return NewMatcher("",
		func(req *http.Request) bool {
			rearmBody(req)
			b, err := io.ReadAll(req.Body)
			return err == nil && bytes.Contains(b, []byte(substr))
		})
}

// HeaderExists returns a [Matcher] checking that request contains
// key header.
//
// The name of the returned [Matcher] is auto-generated (see [NewMatcher]).
// To name it explicitly, use [Matcher.WithName] as in:
//
//	HeaderExists("X-Custom").WithName("10-custom-exists")
//
// See also [github.com/maxatome/tdhttpmock.Header] for powerful
// header testing.
func HeaderExists(key string) Matcher {
	return NewMatcher("",
		func(req *http.Request) bool {
			_, ok := req.Header[key]
			return ok
		})
}

// HeaderIs returns a [Matcher] checking that request contains
// key header set to value.
//
// The name of the returned [Matcher] is auto-generated (see [NewMatcher]).
// To name it explicitly, use [Matcher.WithName] as in:
//
//	HeaderIs("X-Custom", "VALUE").WithName("10-custom-is-value")
//
// See also [github.com/maxatome/tdhttpmock.Header] for powerful
// header testing.
func HeaderIs(key, value string) Matcher {
	return NewMatcher("",
		func(req *http.Request) bool {
			return req.Header.Get(key) == value
		})
}

// HeaderContains returns a [Matcher] checking that request contains key
// header itself containing substr.
//
// The name of the returned [Matcher] is auto-generated (see [NewMatcher]).
// To name it explicitly, use [Matcher.WithName] as in:
//
//	HeaderContains("X-Custom", "VALUE").WithName("10-custom-contains-value")
//
// See also [github.com/maxatome/tdhttpmock.Header] for powerful
// header testing.
func HeaderContains(key, substr string) Matcher {
	return NewMatcher("",
		func(req *http.Request) bool {
			return strings.Contains(req.Header.Get(key), substr)
		})
}

// Name returns the m's name.
func (m Matcher) Name() string {
	return m.name
}

// WithName returns a new [Matcher] based on m with name name.
func (m Matcher) WithName(name string) Matcher {
	return NewMatcher(name, m.fn)
}

// Check returns true if req is matched by m.
func (m Matcher) Check(req *http.Request) bool {
	return m.fn.Check(req)
}

// Or combines m and all ms in a new [Matcher]. This new [Matcher]
// succeeds if one of m or ms succeeds. Note that as a [Matcher]
// succeeds if internal fn is nil, if m's internal fn or any of ms
// item's internal fn is nil, the returned [Matcher] always
// succeeds. The name of returned [Matcher] is m's one.
func (m Matcher) Or(ms ...Matcher) Matcher {
	if len(ms) == 0 || m.fn == nil {
		return m
	}
	mfs := make([]MatcherFunc, 1, len(ms)+1)
	mfs[0] = m.fn
	for _, cur := range ms {
		if cur.fn == nil {
			return Matcher{}
		}
		mfs = append(mfs, cur.fn)
	}
	m.fn = matcherFuncOr(mfs)
	return m
}

// And combines m and all ms in a new [Matcher]. This new [Matcher]
// succeeds if all of m and ms succeed. Note that a [Matcher] also
// succeeds if [Matcher] [MatcherFunc] is nil. The name of returned
// [Matcher] is m's one if the empty/default [Matcher] is returned.
func (m Matcher) And(ms ...Matcher) Matcher {
	if len(ms) == 0 {
		return m
	}
	mfs := make([]MatcherFunc, 0, len(ms)+1)
	if m.fn != nil {
		mfs = append(mfs, m.fn)
	}
	for _, cur := range ms {
		if cur.fn != nil {
			mfs = append(mfs, cur.fn)
		}
	}
	m.fn = matcherFuncAnd(mfs)
	if m.fn != nil {
		return m
	}
	return Matcher{}
}

type matchResponder struct {
	matcher   Matcher
	responder Responder
}

type matchResponders []matchResponder

// add adds or replaces a matchResponder.
func (mrs matchResponders) add(mr matchResponder) matchResponders {
	// default is always at end
	if mr.matcher.fn == nil {
		if len(mrs) > 0 && (mrs)[len(mrs)-1].matcher.fn == nil {
			mrs[len(mrs)-1] = mr
			return mrs
		}
		return append(mrs, mr)
	}

	for i, cur := range mrs {
		if cur.matcher.name == mr.matcher.name {
			mrs[i] = mr
			return mrs
		}
	}

	for i, cur := range mrs {
		if cur.matcher.fn == nil || cur.matcher.name > mr.matcher.name {
			mrs = append(mrs, matchResponder{})
			copy(mrs[i+1:], mrs[i:len(mrs)-1])
			mrs[i] = mr
			return mrs
		}
	}
	return append(mrs, mr)
}

func (mrs matchResponders) checkEmptiness() matchResponders {
	if len(mrs) == 0 {
		return nil
	}
	return mrs
}

func (mrs matchResponders) shrink() matchResponders {
	mrs[len(mrs)-1] = matchResponder{}
	mrs = mrs[:len(mrs)-1]
	return mrs.checkEmptiness()
}

func (mrs matchResponders) remove(name string) matchResponders {
	// Special case, even if default has been renamed, we consider ""
	// matching this default
	if name == "" {
		// default is always at end
		if len(mrs) > 0 && mrs[len(mrs)-1].matcher.fn == nil {
			return mrs.shrink()
		}
		return mrs.checkEmptiness()
	}

	for i, cur := range mrs {
		if cur.matcher.name == name {
			copy(mrs[i:], mrs[i+1:])
			return mrs.shrink()
		}
	}
	return mrs.checkEmptiness()
}

func (mrs matchResponders) findMatchResponder(req *http.Request) *matchResponder {
	if len(mrs) == 0 {
		return nil
	}
	if mrs[0].matcher.fn == nil { // nil match is always the last
		return &mrs[0]
	}

	copyBody := &bodyCopyOnRead{body: req.Body}
	req.Body = copyBody
	defer func() {
		copyBody.rearm()
		req.Body = copyBody.body
	}()

	for _, mr := range mrs {
		copyBody.rearm()
		if mr.matcher.Check(req) {
			return &mr
		}
	}
	return nil
}

type matchRouteKey struct {
	internal.RouteKey
	name string
}

func (m matchRouteKey) String() string {
	if m.name == "" {
		return m.RouteKey.String()
	}
	return m.RouteKey.String() + " <" + m.name + ">"
}

func rearmBody(req *http.Request) {
	if req != nil {
		if body, ok := req.Body.(interface{ rearm() }); ok {
			body.rearm()
		}
	}
}

type buffer struct {
	*bytes.Reader
}

func (b buffer) Close() error {
	return nil
}

// bodyCopyOnRead mutates body into a buffer on first Read(), except
// if body is nil or http.NoBody. In this case, EOF is returned for
// each Read() and body stays untouched.
type bodyCopyOnRead struct {
	body io.ReadCloser
}

func (b *bodyCopyOnRead) rearm() {
	if buf, ok := b.body.(buffer); ok {
		buf.Seek(0, io.SeekStart) //nolint:errcheck
	} // else b.body contains the original body, so don't touch
}

func (b *bodyCopyOnRead) copy() {
	if _, ok := b.body.(buffer); !ok && b.body != nil && b.body != http.NoBody {
		buf, _ := io.ReadAll(b.body)
		b.body.Close() //nolint: errcheck
		b.body = buffer{bytes.NewReader(buf)}
	}
}

func (b *bodyCopyOnRead) Read(p []byte) (n int, err error) {
	b.copy()
	if b.body == nil {
		return 0, io.EOF
	}
	return b.body.Read(p)
}

func (b *bodyCopyOnRead) Close() error {
	return nil
}
