package chromedp

import (
	"context"
	"errors"
	"fmt"
	"io"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/chromedp/cdproto"
	"github.com/chromedp/cdproto/cdp"
	"github.com/chromedp/cdproto/css"
	"github.com/chromedp/cdproto/dom"
	"github.com/chromedp/cdproto/runtime"
)

// QueryAction are element query actions that select node elements from the
// browser's DOM for retrieval or manipulation.
//
// See [Query] for details on building element query selectors.
type QueryAction Action

// Selector holds information pertaining to an element selection query.
//
// See [Query] for information on building an element selector and relevant
// options.
type Selector struct {
	sel           any
	fromNode      *cdp.Node
	retryInterval time.Duration
	exp           int
	by            func(context.Context, *cdp.Node) ([]cdp.NodeID, error)
	wait          func(context.Context, *cdp.Frame, runtime.ExecutionContextID, ...cdp.NodeID) ([]*cdp.Node, error)
	after         []func(context.Context, runtime.ExecutionContextID, ...*cdp.Node) error
}

// Query is a query action that queries the browser for specific element
// node(s) matching the criteria.
//
// Query actions that target a browser DOM element node (or nodes) make use of
// Query, in conjunction with the [After] option to retrieve data or
// to modify the element(s) selected by the query.
//
// For example:
//
//	chromedp.Run(ctx, chromedp.SendKeys(`thing`, chromedp.ByID))
//
// The above will perform a [SendKeys] action on the first element matching a
// browser CSS query for "#thing".
//
// [Element] selection queries work in conjunction with specific actions and form
// the primary way of automating [Tasks] in the browser. They are typically
// written in the following form:
//
//	Action(selector[, parameter1, ...parameterN][,result][, queryOptions...])
//
// Where:
//
//   - Action - the action to perform
//   - selector - element query selection (typically a string), that any matching node(s) will have the action applied
//   - parameter[1-N] - parameter(s) needed for the individual action (if any)
//   - result - pointer to a result (if any)
//   - queryOptions - changes how queries are executed, or how nodes are waited for
//
// # Query Options
//
// By* options specify the type of element query used By the browser to perform
// the selection query. When not specified, element queries will use [BySearch]
// (a wrapper for DOM.performSearch).
//
// Node* options specify node conditions that cause the query to wait until the
// specified condition is true. When not specified, queries will use the
// [NodeReady] wait condition.
//
// The [AtLeast] option alters the minimum number of nodes that must be returned
// by the element query. If not specified, the default value is 1.
//
// The [After] option is used to specify a func that will be executed when
// element query has returned one or more elements, and after the node condition is
// true.
//
// # By Options
//
// The [BySearch] (default) option enables querying for elements by plain text,
// CSS selector or XPath query, wrapping DOM.performSearch.
//
// The [ByID] option enables querying for a single element with the matching CSS
// ID, wrapping DOM.querySelector. ByID is similar to calling
// document.querySelector('#' + ID) from within the browser.
//
// The [ByQuery] option enables querying for a single element using a CSS
// selector, wrapping DOM.querySelector. ByQuery is similar to calling
// document.querySelector() from within the browser.
//
// The [ByQueryAll] option enables querying for elements using a CSS selector,
// wrapping DOM.querySelectorAll. ByQueryAll is similar to calling
// document.querySelectorAll() from within the browser.
//
// The [ByJSPath] option enables querying for a single element using its "JS
// Path" value, wrapping Runtime.evaluate. ByJSPath is similar to executing a
// JavaScript snippet that returns an element from within the browser. ByJSPath
// should be used only with trusted element queries, as it is passed directly
// to Runtime.evaluate, and no attempt is made to sanitize the query. Useful
// for querying DOM elements that cannot be retrieved using other By* funcs,
// such as ShadowDOM elements.
//
// # Node Options
//
// The [NodeReady] (default) option causes the query to wait until all element
// nodes matching the selector have been retrieved from the browser.
//
// The [NodeVisible] option causes the query to wait until all element nodes
// matching the selector have been retrieved from the browser, and are visible.
//
// The [NodeNotVisible] option causes the query to wait until all element nodes
// matching the selector have been retrieved from the browser, and are not
// visible.
//
// The [NodeEnabled] option causes the query to wait until all element nodes
// matching the selector have been retrieved from the browser, and are enabled
// (i.e., do not have a 'disabled' attribute).
//
// The [NodeSelected] option causes the query to wait until all element nodes
// matching the selector have been retrieved from the browser, and are
// selected (i.e., has a 'selected' attribute).
//
// The [NodeNotPresent] option causes the query to wait until there are no
// element nodes matching the selector.
func Query(sel any, opts ...QueryOption) QueryAction {
	s := &Selector{
		sel:           sel,
		exp:           1,
		retryInterval: 5 * time.Millisecond,
	}

	// apply options
	for _, o := range opts {
		o(s)
	}

	if s.by == nil {
		BySearch(s)
	}

	if s.wait == nil {
		NodeReady(s)
	}

	return s
}

// Do executes the selector, only finishing if the selector's by, wait, and
// after funcs succeed, or if the context is cancelled.
func (s *Selector) Do(ctx context.Context) error {
	t := cdp.ExecutorFromContext(ctx).(*Target)
	if t == nil {
		return ErrInvalidTarget
	}
	return retryWithSleep(ctx, s.retryInterval, func(ctx context.Context) (bool, error) {
		frame, root, execCtx, ok := t.ensureFrame()
		if !ok {
			return false, nil
		}

		fromNode := s.fromNode
		if fromNode == nil {
			fromNode = root
		} else {
			frameID := t.enclosingFrame(fromNode)
			t.frameMu.RLock()
			execCtx = t.execContexts[frameID]
			t.frameMu.RUnlock()

			// TODO: we probably want to use the nested frame
			// instead, but note that util.go stores the nested
			// frame's nodes in the root frame's Nodes map.
			// frame = t.frames[fromNode.FrameID]
			// if frame == nil {
			// 	return fmt.Errorf("FromNode provided does not belong to any active frame")
			// }
		}

		// If this is an iframe node, we want to run the query
		// on its "content document" node instead. Otherwise,
		// queries will return no results.
		if doc := fromNode.ContentDocument; doc != nil {
			fromNode = doc
		}

		ids, err := s.by(ctx, fromNode)
		if err != nil {
			var e *cdproto.Error
			// When the selector is invalid (for example, "#a:b" or "#3"), it will
			// always fail with "DOM Error while querying". It does not make sense
			// to retry in this case.
			// Maybe "DOM Error while querying" is also used for other errors other
			// than invalid selector. But the response does not contain anything
			// else that can be used to distinguish them. So we have to go with it.
			if errors.As(err, &e) && e.Message == "DOM Error while querying" {
				return true, err
			}
			return false, nil
		}
		if len(ids) < s.exp {
			return false, nil
		}
		nodes, err := s.wait(ctx, frame, execCtx, ids...)
		// if nodes==nil, we're not yet ready
		if nodes == nil || err != nil {
			return false, nil
		}
		for _, f := range s.after {
			if err := f(ctx, execCtx, nodes...); err != nil {
				return true, err
			}
		}
		return true, nil
	})
}

// selAsString forces sel into a string.
func (s *Selector) selAsString() string {
	if sel, ok := s.sel.(string); ok {
		return sel
	}
	return fmt.Sprintf("%s", s.sel)
}

// waitReady waits for the specified nodes to be ready.
func (s *Selector) waitReady(check func(context.Context, runtime.ExecutionContextID, *cdp.Node) error) func(context.Context, *cdp.Frame, runtime.ExecutionContextID, ...cdp.NodeID) ([]*cdp.Node, error) {
	return func(ctx context.Context, cur *cdp.Frame, execCtx runtime.ExecutionContextID, ids ...cdp.NodeID) ([]*cdp.Node, error) {
		nodes := make([]*cdp.Node, len(ids))
		cur.RLock()
		for i, id := range ids {
			nodes[i] = cur.Nodes[id]
			if nodes[i] == nil {
				cur.RUnlock()
				// not yet ready
				return nil, nil
			}
		}
		cur.RUnlock()

		if check != nil {
			errc := make(chan error, 1)
			for _, n := range nodes {
				go func(n *cdp.Node) {
					select {
					case <-ctx.Done():
						errc <- ctx.Err()
					case errc <- check(ctx, execCtx, n):
					}
				}(n)
			}

			var first error
			for range nodes {
				if err := <-errc; first == nil {
					first = err
				}
			}
			close(errc)
			if first != nil {
				return nil, first
			}
		}
		return nodes, nil
	}
}

// QueryAfter is an element query action that queries the browser for selector
// sel. Waits until the visibility conditions of the query have been met, after
// which executes f.
func QueryAfter(sel any, f func(context.Context, runtime.ExecutionContextID, ...*cdp.Node) error, opts ...QueryOption) QueryAction {
	return Query(sel, append(opts, After(f))...)
}

// QueryOption is an element query action option.
type QueryOption = func(*Selector)

// FromNode is an element query action option where a query will be run. That
// is, the query will only look at the node's element sub-tree. By default, or
// when passed nil, the document's root element will be used.
//
// Note that, at present, BySearch and ByJSPath do not support FromNode; this
// option is mainly useful for ByQuery selectors.
func FromNode(node *cdp.Node) QueryOption {
	return func(s *Selector) { s.fromNode = node }
}

// ByFunc is an element query action option to set the func used to select elements.
func ByFunc(f func(context.Context, *cdp.Node) ([]cdp.NodeID, error)) QueryOption {
	return func(s *Selector) {
		s.by = f
	}
}

// ByQuery is an element query action option to select a single element by the
// DOM.querySelector command.
//
// Similar to calling document.querySelector() in the browser.
func ByQuery(s *Selector) {
	ByFunc(func(ctx context.Context, n *cdp.Node) ([]cdp.NodeID, error) {
		nodeID, err := dom.QuerySelector(n.NodeID, s.selAsString()).Do(ctx)
		if err != nil {
			return nil, err
		}

		if nodeID == cdp.EmptyNodeID {
			return []cdp.NodeID{}, nil
		}

		return []cdp.NodeID{nodeID}, nil
	})(s)
}

// ByQueryAll is an element query action option to select elements by the
// DOM.querySelectorAll command.
//
// Similar to calling document.querySelectorAll() in the browser.
func ByQueryAll(s *Selector) {
	ByFunc(func(ctx context.Context, n *cdp.Node) ([]cdp.NodeID, error) {
		return dom.QuerySelectorAll(n.NodeID, s.selAsString()).Do(ctx)
	})(s)
}

// ByID is an element query option to select a single element by its CSS #id.
//
// Similar to calling document.querySelector('#' + ID) in the browser.
func ByID(s *Selector) {
	s.sel = "#" + strings.TrimPrefix(s.selAsString(), "#")
	ByQuery(s)
}

// BySearch is an element query option to select elements by the DOM.performSearch
// command. It matches nodes by plain text, CSS selector or XPath query.
func BySearch(s *Selector) {
	ByFunc(func(ctx context.Context, n *cdp.Node) ([]cdp.NodeID, error) {
		id, count, err := dom.PerformSearch(s.selAsString()).Do(ctx)
		if err != nil {
			return nil, err
		}

		defer func() {
			_ = dom.DiscardSearchResults(id).Do(ctx)
		}()

		if count < 1 {
			return []cdp.NodeID{}, nil
		}

		nodes, err := dom.GetSearchResults(id, 0, count).Do(ctx)
		if err != nil {
			return nil, err
		}

		return nodes, nil
	})(s)
}

// ByJSPath is an element query option to select elements by the "JS Path"
// value (as shown in the Chrome DevTools UI).
//
// Allows for the direct querying of DOM elements that otherwise cannot be
// retrieved using the other By* funcs, such as ShadowDOM elements.
//
// Note: Do not use with an untrusted selector value, as any defined selector
// will be passed to runtime.Evaluate.
func ByJSPath(s *Selector) {
	ByFunc(func(ctx context.Context, n *cdp.Node) ([]cdp.NodeID, error) {
		// set up eval command
		p := runtime.Evaluate(s.selAsString()).
			WithAwaitPromise(true).
			WithObjectGroup("console").
			WithIncludeCommandLineAPI(true)

		// execute
		v, exp, err := p.Do(ctx)
		if err != nil {
			return nil, err
		}
		if exp != nil {
			return nil, exp
		}

		// use the ObjectID from the evaluation to get the nodeID
		nodeID, err := dom.RequestNode(v.ObjectID).Do(ctx)
		if err != nil {
			return nil, err
		}

		if nodeID == cdp.EmptyNodeID {
			return []cdp.NodeID{}, nil
		}

		return []cdp.NodeID{nodeID}, nil
	})(s)
}

// ByNodeID is an element query option to select elements by their node IDs.
//
// Uses DOM.requestChildNodes to retrieve elements with specific node IDs.
//
// Note: must be used with []cdp.NodeID.
func ByNodeID(s *Selector) {
	ids, ok := s.sel.([]cdp.NodeID)
	if !ok {
		panic("ByNodeID can only work on []cdp.NodeID")
	}

	ByFunc(func(ctx context.Context, n *cdp.Node) ([]cdp.NodeID, error) {
		for _, id := range ids {
			err := dom.RequestChildNodes(id).WithPierce(true).Do(ctx)
			if err != nil {
				return nil, err
			}
		}

		return ids, nil
	})(s)
}

// WaitFunc is an element query option to set a custom node condition wait.
func WaitFunc(wait func(context.Context, *cdp.Frame, runtime.ExecutionContextID, ...cdp.NodeID) ([]*cdp.Node, error)) QueryOption {
	return func(s *Selector) {
		s.wait = wait
	}
}

// NodeReady is an element query option to wait until all queried element nodes
// have been sent by the browser.
func NodeReady(s *Selector) {
	WaitFunc(s.waitReady(nil))(s)
}

func callFunctionOnNode(ctx context.Context, node *cdp.Node, function string, res any, args ...any) error {
	r, err := dom.ResolveNode().WithNodeID(node.NodeID).Do(ctx)
	if err != nil {
		return err
	}
	err = CallFunctionOn(function, res,
		func(p *runtime.CallFunctionOnParams) *runtime.CallFunctionOnParams {
			return p.WithObjectID(r.ObjectID)
		},
		args...,
	).Do(ctx)
	if err != nil {
		return err
	}

	// Try to release the remote object.
	// It will fail if the page is navigated or closed,
	// and it's okay to ignore the error in this case.
	_ = runtime.ReleaseObject(r.ObjectID).Do(ctx)

	return nil
}

// NodeVisible is an element query option to wait until all queried element
// nodes have been sent by the browser and are visible.
func NodeVisible(s *Selector) {
	WaitFunc(s.waitReady(func(ctx context.Context, execCtx runtime.ExecutionContextID, n *cdp.Node) error {
		// check box model
		_, err := dom.GetBoxModel().WithNodeID(n.NodeID).Do(ctx)
		if err != nil {
			if isCouldNotComputeBoxModelError(err) {
				return ErrNotVisible
			}

			return err
		}

		// check visibility
		var res bool
		err = callFunctionOnNode(ctx, n, visibleJS, &res)
		if err != nil {
			return err
		}
		if !res {
			return ErrNotVisible
		}
		return nil
	}))(s)
}

// NodeNotVisible is an element query option to wait until all queried element
// nodes have been sent by the browser and are not visible.
func NodeNotVisible(s *Selector) {
	WaitFunc(s.waitReady(func(ctx context.Context, execCtx runtime.ExecutionContextID, n *cdp.Node) error {
		// check box model
		_, err := dom.GetBoxModel().WithNodeID(n.NodeID).Do(ctx)
		if err != nil {
			if isCouldNotComputeBoxModelError(err) {
				return nil
			}

			return err
		}

		// check visibility
		var res bool
		err = callFunctionOnNode(ctx, n, visibleJS, &res)
		if err != nil {
			return err
		}
		if res {
			return ErrVisible
		}
		return nil
	}))(s)
}

// NodeEnabled is an element query option to wait until all queried element
// nodes have been sent by the browser and are enabled (i.e., do not have a
// 'disabled' attribute).
func NodeEnabled(s *Selector) {
	WaitFunc(s.waitReady(func(ctx context.Context, execCtx runtime.ExecutionContextID, n *cdp.Node) error {
		n.RLock()
		defer n.RUnlock()

		for i := 0; i < len(n.Attributes); i += 2 {
			if n.Attributes[i] == "disabled" {
				return ErrDisabled
			}
		}

		return nil
	}))(s)
}

// NodeSelected is an element query option to wait until all queried element
// nodes have been sent by the browser and are selected (i.e., has 'selected'
// attribute).
func NodeSelected(s *Selector) {
	WaitFunc(s.waitReady(func(ctx context.Context, execCtx runtime.ExecutionContextID, n *cdp.Node) error {
		n.RLock()
		defer n.RUnlock()

		for i := 0; i < len(n.Attributes); i += 2 {
			if n.Attributes[i] == "selected" {
				return nil
			}
		}

		return ErrNotSelected
	}))(s)
}

// NodeNotPresent is an element query option to wait until no elements are
// present that match the query.
//
// Note: forces the expected number of element nodes to be 0.
func NodeNotPresent(s *Selector) {
	s.exp = 0
	WaitFunc(func(ctx context.Context, cur *cdp.Frame, execCtx runtime.ExecutionContextID, ids ...cdp.NodeID) ([]*cdp.Node, error) {
		if len(ids) != 0 {
			return nil, ErrHasResults
		}
		return []*cdp.Node{}, nil
	})(s)
}

// AtLeast is an element query option to set a minimum number of elements that
// must be returned by the query.
//
// By default, a query will have a value of 1.
func AtLeast(n int) QueryOption {
	return func(s *Selector) {
		s.exp = n
	}
}

// RetryInterval is an element query action option to set the retry interval to specify
// how often it should retry when it failed to select the target element(s).
//
// The default value is 5ms.
func RetryInterval(interval time.Duration) QueryOption {
	return func(s *Selector) {
		s.retryInterval = interval
	}
}

// After is an element query option that sets a func to execute after the
// matched nodes have been returned by the browser, and after the node
// condition is true.
func After(f func(context.Context, runtime.ExecutionContextID, ...*cdp.Node) error) QueryOption {
	return func(s *Selector) {
		s.after = append(s.after, f)
	}
}

// Populate is an element query option that causes the queried nodes to be
// retrieved for later use. Use a depth of -1 to retrieve all child nodes. When
// pierce is true, will pierce child containers (e.g. iframes and the like)
//
// NOTE: this could be extremely resource intensive. Avoid doing this unless
// necessary.
func Populate(depth int64, pierce bool, opts ...PopulateOption) QueryOption {
	return After(func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		var d time.Duration
		for _, o := range opts {
			o(&d)
		}
		for _, n := range nodes {
			if err := dom.RequestChildNodes(n.NodeID).
				WithDepth(depth).
				WithPierce(pierce).
				Do(ctx); err != nil {
				return err
			}
		}
		if d != 0 {
			<-time.After(d)
		}
		return nil
	})
}

// PopulateOption is an element populate action option.
type PopulateOption = func(*time.Duration)

// PopulateWait is populate option to set a wait interval after requesting
// child nodes.
func PopulateWait(wait time.Duration) PopulateOption {
	return func(d *time.Duration) {
		*d = wait
	}
}

// WaitReady is an element query action that waits until the element matching
// the selector is ready (i.e., has been "loaded").
func WaitReady(sel any, opts ...QueryOption) QueryAction {
	return Query(sel, opts...)
}

// WaitVisible is an element query action that waits until the element matching
// the selector is visible.
func WaitVisible(sel any, opts ...QueryOption) QueryAction {
	return Query(sel, append(opts, NodeVisible)...)
}

// WaitNotVisible is an element query action that waits until the element
// matching the selector is not visible.
func WaitNotVisible(sel any, opts ...QueryOption) QueryAction {
	return Query(sel, append(opts, NodeNotVisible)...)
}

// WaitEnabled is an element query action that waits until the element matching
// the selector is enabled (i.e., does not have attribute 'disabled').
func WaitEnabled(sel any, opts ...QueryOption) QueryAction {
	return Query(sel, append(opts, NodeEnabled)...)
}

// WaitSelected is an element query action that waits until the element
// matching the selector is selected (i.e., has attribute 'selected').
func WaitSelected(sel any, opts ...QueryOption) QueryAction {
	return Query(sel, append(opts, NodeSelected)...)
}

// WaitNotPresent is an element query action that waits until no elements are
// present matching the selector.
func WaitNotPresent(sel any, opts ...QueryOption) QueryAction {
	return Query(sel, append(opts, NodeNotPresent)...)
}

// Nodes is an element query action that retrieves the document element nodes
// matching the selector.
func Nodes(sel any, nodes *[]*cdp.Node, opts ...QueryOption) QueryAction {
	if nodes == nil {
		panic("nodes cannot be nil")
	}

	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, n ...*cdp.Node) error {
		*nodes = n
		return nil
	}, opts...)
}

// NodeIDs is an element query action that retrieves the element node IDs matching the
// selector.
func NodeIDs(sel any, ids *[]cdp.NodeID, opts ...QueryOption) QueryAction {
	if ids == nil {
		panic("nodes cannot be nil")
	}

	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		nodeIDs := make([]cdp.NodeID, len(nodes))
		for i, n := range nodes {
			nodeIDs[i] = n.NodeID
		}

		*ids = nodeIDs

		return nil
	}, opts...)
}

// Focus is an element query action that focuses the first element node matching the
// selector.
func Focus(sel any, opts ...QueryOption) QueryAction {
	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		if len(nodes) < 1 {
			return fmt.Errorf("selector %q did not return any nodes", sel)
		}

		return dom.Focus().WithNodeID(nodes[0].NodeID).Do(ctx)
	}, opts...)
}

// Blur is an element query action that unfocuses (blurs) the first element node
// matching the selector.
func Blur(sel any, opts ...QueryOption) QueryAction {
	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		if len(nodes) < 1 {
			return fmt.Errorf("selector %q did not return any nodes", sel)
		}

		var res bool
		err := callFunctionOnNode(ctx, nodes[0], blurJS, &res)
		if err != nil {
			return err
		}

		if !res {
			return fmt.Errorf("could not blur node %d", nodes[0].NodeID)
		}

		return nil
	}, opts...)
}

// Dimensions is an element query action that retrieves the box model dimensions for the
// first element node matching the selector.
func Dimensions(sel any, model **dom.BoxModel, opts ...QueryOption) QueryAction {
	if model == nil {
		panic("model cannot be nil")
	}
	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		if len(nodes) < 1 {
			return fmt.Errorf("selector %q did not return any nodes", sel)
		}
		var err error
		*model, err = dom.GetBoxModel().WithNodeID(nodes[0].NodeID).Do(ctx)
		return err
	}, opts...)
}

// Text is an element query action that retrieves the visible text of the first element
// node matching the selector.
func Text(sel any, text *string, opts ...QueryOption) QueryAction {
	if text == nil {
		panic("text cannot be nil")
	}

	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		if len(nodes) < 1 {
			return fmt.Errorf("selector %q did not return any nodes", sel)
		}

		return callFunctionOnNode(ctx, nodes[0], textJS, text)
	}, opts...)
}

// TextContent is an element query action that retrieves the text content of the first element
// node matching the selector.
func TextContent(sel any, text *string, opts ...QueryOption) QueryAction {
	if text == nil {
		panic("text cannot be nil")
	}

	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		if len(nodes) < 1 {
			return fmt.Errorf("selector %q did not return any nodes", sel)
		}

		return callFunctionOnNode(ctx, nodes[0], textContentJS, text)
	}, opts...)
}

// Clear is an element query action that clears the values of any input/textarea element
// nodes matching the selector.
func Clear(sel any, opts ...QueryOption) QueryAction {
	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		if len(nodes) < 1 {
			return fmt.Errorf("selector %q did not return any nodes", sel)
		}

		for _, n := range nodes {
			if n.NodeType != cdp.NodeTypeElement || (n.NodeName != "INPUT" && n.NodeName != "TEXTAREA") {
				return fmt.Errorf("selector %q matched node %d with name %s", sel, n.NodeID, strings.ToLower(n.NodeName))
			}
		}

		errs := make([]error, len(nodes))
		var wg sync.WaitGroup
		for i, n := range nodes {
			wg.Add(1)
			go func(i int, n *cdp.Node) {
				defer wg.Done()

				var a Action
				if n.NodeName == "INPUT" {
					a = dom.SetAttributeValue(n.NodeID, "value", "")
				} else {
					// find textarea's child #text node
					var textID cdp.NodeID
					var found bool
					for _, c := range n.Children {
						if c.NodeType == cdp.NodeTypeText {
							textID = c.NodeID
							found = true
							break
						}
					}

					if !found {
						errs[i] = fmt.Errorf("textarea node %d does not have child #text node", n.NodeID)
						return
					}

					a = dom.SetNodeValue(textID, "")
				}
				errs[i] = a.Do(ctx)
			}(i, n)
		}
		wg.Wait()

		for _, err := range errs {
			if err != nil {
				return err
			}
		}

		return nil
	}, opts...)
}

// Value is an element query action that retrieves the JavaScript value field of the
// first element node matching the selector.
//
// Useful for retrieving an element's JavaScript value, namely form, input,
// textarea, select, or any other element with a '.value' field.
func Value(sel any, value *string, opts ...QueryOption) QueryAction {
	if value == nil {
		panic("value cannot be nil")
	}

	return JavascriptAttribute(sel, "value", value, opts...)
}

// SetValue is an element query action that sets the JavaScript value of the first
// element node matching the selector.
//
// Useful for setting an element's JavaScript value, namely form, input,
// textarea, select, or other element with a '.value' field.
func SetValue(sel any, value string, opts ...QueryOption) QueryAction {
	return SetJavascriptAttribute(sel, "value", value, opts...)
}

// Attributes is an element query action that retrieves the element attributes for the
// first element node matching the selector.
func Attributes(sel any, attributes *map[string]string, opts ...QueryOption) QueryAction {
	if attributes == nil {
		panic("attributes cannot be nil")
	}

	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		if len(nodes) < 1 {
			return fmt.Errorf("selector %q did not return any nodes", sel)
		}

		nodes[0].RLock()
		defer nodes[0].RUnlock()

		m := make(map[string]string)
		attrs := nodes[0].Attributes
		for i := 0; i < len(attrs); i += 2 {
			m[attrs[i]] = attrs[i+1]
		}

		*attributes = m

		return nil
	}, opts...)
}

// AttributesAll is an element query action that retrieves the element attributes for
// all element nodes matching the selector.
//
// Note: this should be used with the ByQueryAll query option.
func AttributesAll(sel any, attributes *[]map[string]string, opts ...QueryOption) QueryAction {
	if attributes == nil {
		panic("attributes cannot be nil")
	}

	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		if len(nodes) < 1 {
			return fmt.Errorf("selector %q did not return any nodes", sel)
		}

		for _, node := range nodes {
			node.RLock()
			m := make(map[string]string)
			attrs := node.Attributes
			for i := 0; i < len(attrs); i += 2 {
				m[attrs[i]] = attrs[i+1]
			}
			*attributes = append(*attributes, m)
			node.RUnlock()
		}
		return nil
	}, opts...)
}

// SetAttributes is an element query action that sets the element attributes for the
// first element node matching the selector.
func SetAttributes(sel any, attributes map[string]string, opts ...QueryOption) QueryAction {
	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		if len(nodes) < 1 {
			return errors.New("expected at least one element")
		}

		i, attrs := 0, make([]string, len(attributes))
		for k, v := range attributes {
			attrs[i] = fmt.Sprintf(`%s=%s`, k, strconv.Quote(v))
			i++
		}

		return dom.SetAttributesAsText(nodes[0].NodeID, strings.Join(attrs, " ")).Do(ctx)
	}, opts...)
}

// AttributeValue is an element query action that retrieves the element attribute value
// for the first element node matching the selector.
func AttributeValue(sel any, name string, value *string, ok *bool, opts ...QueryOption) QueryAction {
	if value == nil {
		panic("value cannot be nil")
	}

	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		if len(nodes) < 1 {
			return errors.New("expected at least one element")
		}

		nodes[0].RLock()
		defer nodes[0].RUnlock()

		attrs := nodes[0].Attributes
		for i := 0; i < len(attrs); i += 2 {
			if attrs[i] == name {
				*value = attrs[i+1]
				if ok != nil {
					*ok = true
				}
				return nil
			}
		}

		if ok != nil {
			*ok = false
		}

		return nil
	}, opts...)
}

// SetAttributeValue is an element query action that sets the element attribute with
// name to value for the first element node matching the selector.
func SetAttributeValue(sel any, name, value string, opts ...QueryOption) QueryAction {
	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		if len(nodes) < 1 {
			return fmt.Errorf("selector %q did not return any nodes", sel)
		}

		return dom.SetAttributeValue(nodes[0].NodeID, name, value).Do(ctx)
	}, opts...)
}

// RemoveAttribute is an element query action that removes the element attribute with
// name from the first element node matching the selector.
func RemoveAttribute(sel any, name string, opts ...QueryOption) QueryAction {
	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		if len(nodes) < 1 {
			return fmt.Errorf("selector %q did not return any nodes", sel)
		}

		return dom.RemoveAttribute(nodes[0].NodeID, name).Do(ctx)
	}, opts...)
}

// JavascriptAttribute is an element query action that retrieves the JavaScript
// attribute for the first element node matching the selector.
func JavascriptAttribute(sel any, name string, res any, opts ...QueryOption) QueryAction {
	if res == nil {
		panic("res cannot be nil")
	}
	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		if len(nodes) < 1 {
			return fmt.Errorf("selector %q did not return any nodes", sel)
		}

		if err := callFunctionOnNode(ctx, nodes[0], attributeJS, res, name); err != nil {
			return fmt.Errorf("could not retrieve attribute %q: %w", name, err)
		}

		return nil
	}, opts...)
}

// SetJavascriptAttribute is an element query action that sets the JavaScript attribute
// for the first element node matching the selector.
func SetJavascriptAttribute(sel any, name, value string, opts ...QueryOption) QueryAction {
	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		if len(nodes) < 1 {
			return fmt.Errorf("selector %q did not return any nodes", sel)
		}

		var res string
		err := callFunctionOnNode(ctx, nodes[0], setAttributeJS, &res, name, value)
		if err != nil {
			return err
		}
		if res != value {
			return fmt.Errorf("could not set value on node %d", nodes[0].NodeID)
		}

		return nil
	}, opts...)
}

// OuterHTML is an element query action that retrieves the outer html of the first
// element node matching the selector.
func OuterHTML(sel any, html *string, opts ...QueryOption) QueryAction {
	if html == nil {
		panic("html cannot be nil")
	}
	return JavascriptAttribute(sel, "outerHTML", html, opts...)
}

// InnerHTML is an element query action that retrieves the inner html of the first
// element node matching the selector.
func InnerHTML(sel any, html *string, opts ...QueryOption) QueryAction {
	if html == nil {
		panic("html cannot be nil")
	}
	return JavascriptAttribute(sel, "innerHTML", html, opts...)
}

// Click is an element query action that sends a mouse click event to the first element
// node matching the selector.
func Click(sel any, opts ...QueryOption) QueryAction {
	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		if len(nodes) < 1 {
			return fmt.Errorf("selector %q did not return any nodes", sel)
		}

		return MouseClickNode(nodes[0]).Do(ctx)
	}, append(opts, NodeVisible)...)
}

// DoubleClick is an element query action that sends a mouse double click event to the
// first element node matching the selector.
func DoubleClick(sel any, opts ...QueryOption) QueryAction {
	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		if len(nodes) < 1 {
			return fmt.Errorf("selector %q did not return any nodes", sel)
		}

		return MouseClickNode(nodes[0], ClickCount(2)).Do(ctx)
	}, append(opts, NodeVisible)...)
}

// SendKeys is an element query action that synthesizes the key up, char, and down
// events as needed for the runes in v, sending them to the first element node
// matching the selector.
//
// See the [keys] for a complete example on how to use SendKeys.
//
// Note: when the element query matches an input[type="file"] node, then
// dom.SetFileInputFiles is used to set the upload path of the input node to v.
//
// [keys]: https://github.com/chromedp/examples/tree/master/keys
func SendKeys(sel any, v string, opts ...QueryOption) QueryAction {
	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		if len(nodes) < 1 {
			return fmt.Errorf("selector %q did not return any nodes", sel)
		}

		n := nodes[0]

		// grab type attribute from node
		typ, attrs := "", n.Attributes
		n.RLock()
		for i := 0; i < len(attrs); i += 2 {
			if attrs[i] == "type" {
				typ = attrs[i+1]
			}
		}
		n.RUnlock()

		// when working with input[type="file"], call dom.SetFileInputFiles
		if n.NodeName == "INPUT" && typ == "file" {
			return dom.SetFileInputFiles([]string{v}).WithNodeID(n.NodeID).Do(ctx)
		}

		return KeyEventNode(n, v).Do(ctx)
	}, append(opts, NodeVisible)...)
}

// SetUploadFiles is an element query action that sets the files to upload (i.e., for a
// input[type="file"] node) for the first element node matching the selector.
func SetUploadFiles(sel any, files []string, opts ...QueryOption) QueryAction {
	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		if len(nodes) < 1 {
			return fmt.Errorf("selector %q did not return any nodes", sel)
		}

		return dom.SetFileInputFiles(files).WithNodeID(nodes[0].NodeID).Do(ctx)
	}, opts...)
}

// Submit is an element query action that submits the parent form of the first element
// node matching the selector.
func Submit(sel any, opts ...QueryOption) QueryAction {
	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		if len(nodes) < 1 {
			return fmt.Errorf("selector %q did not return any nodes", sel)
		}

		var res bool
		err := callFunctionOnNode(ctx, nodes[0], submitJS, &res)
		if err != nil {
			return err
		}

		if !res {
			return fmt.Errorf("could not call submit on node %d", nodes[0].NodeID)
		}

		return nil
	}, opts...)
}

// Reset is an element query action that resets the parent form of the first element
// node matching the selector.
func Reset(sel any, opts ...QueryOption) QueryAction {
	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		if len(nodes) < 1 {
			return fmt.Errorf("selector %q did not return any nodes", sel)
		}

		var res bool
		err := callFunctionOnNode(ctx, nodes[0], resetJS, &res)
		if err != nil {
			return err
		}

		if !res {
			return fmt.Errorf("could not call reset on node %d", nodes[0].NodeID)
		}

		return nil
	}, opts...)
}

// ComputedStyle is an element query action that retrieves the computed style of the
// first element node matching the selector.
func ComputedStyle(sel any, style *[]*css.ComputedStyleProperty, opts ...QueryOption) QueryAction {
	if style == nil {
		panic("style cannot be nil")
	}

	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		if len(nodes) < 1 {
			return fmt.Errorf("selector %q did not return any nodes", sel)
		}

		computed, _, err := css.GetComputedStyleForNode(nodes[0].NodeID).Do(ctx)
		if err != nil {
			return err
		}

		*style = computed

		return nil
	}, opts...)
}

// MatchedStyle is an element query action that retrieves the matched style information
// for the first element node matching the selector.
func MatchedStyle(sel any, style **css.GetMatchedStylesForNodeReturns, opts ...QueryOption) QueryAction {
	if style == nil {
		panic("style cannot be nil")
	}

	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		if len(nodes) < 1 {
			return fmt.Errorf("selector %q did not return any nodes", sel)
		}

		var err error
		ret := &css.GetMatchedStylesForNodeReturns{}
		p := css.GetMatchedStylesForNode(nodes[0].NodeID)
		err = cdp.Execute(ctx, css.CommandGetMatchedStylesForNode, p, ret)
		if err != nil {
			return err
		}

		*style = ret

		return nil
	}, opts...)
}

// ScrollIntoView is an element query action that scrolls the window to the
// first element node matching the selector.
func ScrollIntoView(sel any, opts ...QueryOption) QueryAction {
	return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
		if len(nodes) < 1 {
			return fmt.Errorf("selector %q did not return any nodes", sel)
		}

		return dom.ScrollIntoViewIfNeeded().WithNodeID(nodes[0].NodeID).Do(ctx)
	}, opts...)
}

// DumpTo is an element query action that writes a readable tree of the first
// element node matching the selector and its children, up to the specified
// depth.
//
// See [Dump] for a simpler interface.
func DumpTo(sel any, w io.Writer, prefix, indent string, nodeIDs bool, depth int64, pierce bool, wait time.Duration, opts ...QueryOption) QueryAction {
	return Query(sel, append(opts,
		Populate(depth, pierce, PopulateWait(wait)),
		After(func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
			var n *cdp.Node
			if len(nodes) > 0 {
				n = nodes[0]
			}
			_, err := n.WriteTo(w, prefix, indent, nodeIDs)
			return err
		}),
	)...)
}

// Dump is an element query action that writes a readable tree of the first
// element node matching the selector and its children, up to the specified
// depth.
//
// See [DumpTo] for more configurable options, which includes the ability to
// set the sleep wait timeout.
func Dump(sel any, w io.Writer, opts ...QueryOption) QueryAction {
	return DumpTo(sel, w, "", "  ", false, -1, true, 80*time.Millisecond, opts...)
}
