package components

import (
	"fmt"
	"regexp"
	"strings"

	"github.com/samber/lo"
)

type TextMatcher struct {
	// If you add or change a field here, be sure to update the copy
	// code in checkIsSelected()
	*Matcher[string]
}

func (self *TextMatcher) Contains(target string) *TextMatcher {
	self.appendRule(matcherRule[string]{
		name: fmt.Sprintf("contains '%s'", target),
		testFn: func(value string) (bool, string) {
			// everything contains the empty string so we unconditionally return true here
			if target == "" {
				return true, ""
			}

			return strings.Contains(value, target), fmt.Sprintf("Expected '%s' to be found in '%s'", target, value)
		},
	})

	return self
}

func (self *TextMatcher) DoesNotContain(target string) *TextMatcher {
	self.appendRule(matcherRule[string]{
		name: fmt.Sprintf("does not contain '%s'", target),
		testFn: func(value string) (bool, string) {
			return !strings.Contains(value, target), fmt.Sprintf("Expected '%s' to NOT be found in '%s'", target, value)
		},
	})

	return self
}

func (self *TextMatcher) DoesNotContainAnyOf(targets []string) *TextMatcher {
	self.appendRule(matcherRule[string]{
		name: fmt.Sprintf("does not contain any of '%s'", targets),
		testFn: func(value string) (bool, string) {
			return lo.NoneBy(targets, func(target string) bool { return strings.Contains(value, target) }),
				fmt.Sprintf("Expected none of '%s' to be found in '%s'", targets, value)
		},
	})

	return self
}

func (self *TextMatcher) MatchesRegexp(target string) *TextMatcher {
	self.appendRule(matcherRule[string]{
		name: fmt.Sprintf("matches regular expression '%s'", target),
		testFn: func(value string) (bool, string) {
			matched, err := regexp.MatchString(target, value)
			if err != nil {
				return false, fmt.Sprintf("Unexpected error parsing regular expression '%s': %s", target, err.Error())
			}
			return matched, fmt.Sprintf("Expected '%s' to match regular expression /%s/", value, target)
		},
	})

	return self
}

func (self *TextMatcher) Equals(target string) *TextMatcher {
	self.appendRule(matcherRule[string]{
		name: fmt.Sprintf("equals '%s'", target),
		testFn: func(value string) (bool, string) {
			return target == value, fmt.Sprintf("Expected '%s' to equal '%s'", value, target)
		},
	})

	return self
}

const IS_SELECTED_RULE_NAME = "is selected"

// special rule that is only to be used in the TopLines and Lines methods, as a way of
// asserting that a given line is selected.
func (self *TextMatcher) IsSelected() *TextMatcher {
	self.appendRule(matcherRule[string]{
		name: IS_SELECTED_RULE_NAME,
		testFn: func(value string) (bool, string) {
			panic("Special IsSelected matcher is not supposed to have its testFn method called. This rule should only be used within the .Lines() and .TopLines() method on a ViewAsserter.")
		},
	})

	return self
}

// if the matcher has an `IsSelected` rule, it returns true, along with the matcher after that rule has been removed
func (self *TextMatcher) checkIsSelected() (bool, *TextMatcher) {
	// copying into a new matcher in case we want to reuse the original later
	newMatcher := &TextMatcher{Matcher: &Matcher[string]{}}
	*newMatcher.Matcher = *self.Matcher

	check := lo.ContainsBy(newMatcher.rules, func(rule matcherRule[string]) bool { return rule.name == IS_SELECTED_RULE_NAME })

	newMatcher.rules = lo.Filter(newMatcher.rules, func(rule matcherRule[string], _ int) bool { return rule.name != IS_SELECTED_RULE_NAME })

	return check, newMatcher
}

// this matcher has no rules meaning it always passes the test. Use this
// when you don't care what value you're dealing with.
func AnyString() *TextMatcher {
	return &TextMatcher{Matcher: &Matcher[string]{}}
}

func Contains(target string) *TextMatcher {
	return AnyString().Contains(target)
}

func DoesNotContain(target string) *TextMatcher {
	return AnyString().DoesNotContain(target)
}

func DoesNotContainAnyOf(targets ...string) *TextMatcher {
	return AnyString().DoesNotContainAnyOf(targets)
}

func MatchesRegexp(target string) *TextMatcher {
	return AnyString().MatchesRegexp(target)
}

func Equals(target string) *TextMatcher {
	return AnyString().Equals(target)
}
