///|
pub(all) enum RuleProfile {
  BasicProfile
  BoundaryProfile
  ExperimentalProfile
} derive(Debug, Eq, ToJson, FromJson)

///|
pub(all) struct RuleCatalogEntry {
  rule : MutationRule
  profile : RuleProfile
  description : String
} derive(Debug, Eq, ToJson)

///|
pub(all) struct RuleSetValidation {
  rule_count : Int
  duplicate_labels : Array[String]
  empty_tokens : Array[String]
} derive(Debug, Eq, ToJson)

///|
pub fn profile_label(profile : RuleProfile) -> String {
  match profile {
    BasicProfile => "basic"
    BoundaryProfile => "boundary"
    ExperimentalProfile => "experimental"
  }
}

///|
pub fn mutation_kind_label(kind : MutationKind) -> String {
  match kind {
    BooleanLiteral => "boolean"
    EqualityOperator => "equality"
    RelationalOperator => "relational"
    ArithmeticOperator => "arithmetic"
    LogicalOperator => "logical"
    NumericLiteral => "numeric"
  }
}

///|
pub fn parse_rule_profile(text : String) -> RuleProfile? {
  match text {
    "basic" | "BasicProfile" => Some(BasicProfile)
    "boundary" | "BoundaryProfile" => Some(BoundaryProfile)
    "experimental" | "ExperimentalProfile" => Some(ExperimentalProfile)
    _ => None
  }
}

///|
pub fn rule(
  kind : MutationKind,
  from : String,
  to : String,
  label : String,
) -> MutationRule {
  { kind, from, to, label }
}

///|
pub fn rule_catalog() -> Array[RuleCatalogEntry] {
  let entries : Array[RuleCatalogEntry] = []
  for rule in default_rules() {
    entries.push({
      rule,
      profile: BasicProfile,
      description: basic_rule_description(rule.label),
    })
  }
  for rule in boundary_literal_rules() {
    entries.push({
      rule,
      profile: BoundaryProfile,
      description: boundary_rule_description(rule.label),
    })
  }
  for rule in experimental_rules() {
    entries.push({
      rule,
      profile: ExperimentalProfile,
      description: experimental_rule_description(rule.label),
    })
  }
  entries
}

///|
pub fn rules_for_profile(profile : RuleProfile) -> Array[MutationRule] {
  match profile {
    BasicProfile => default_rules()
    BoundaryProfile => boundary_literal_rules() + default_rules()
    ExperimentalProfile =>
      experimental_rules() + boundary_literal_rules() + default_rules()
  }
}

///|
pub fn boundary_literal_rules() -> Array[MutationRule] {
  [
    rule(NumericLiteral, "0", "1", "zero-to-one"),
    rule(NumericLiteral, "1", "0", "one-to-zero"),
    rule(NumericLiteral, "-1", "0", "minus-one-to-zero"),
  ]
}

///|
/// Strong operator replacements intended for a small, opt-in experimental run.
pub fn experimental_rules() -> Array[MutationRule] {
  [
    rule(RelationalOperator, "<=", ">", "le-to-gt"),
    rule(RelationalOperator, "<", ">=", "lt-to-ge"),
    rule(RelationalOperator, ">=", "<", "ge-to-lt"),
    rule(RelationalOperator, ">", "<=", "gt-to-le"),
    rule(ArithmeticOperator, "+", "*", "add-to-mul"),
    rule(ArithmeticOperator, "-", "*", "sub-to-mul"),
    rule(ArithmeticOperator, "*", "+", "mul-to-add"),
    rule(ArithmeticOperator, "/", "+", "div-to-add"),
  ]
}

///|
pub fn discover_with_profile(
  source : String,
  profile : RuleProfile,
  file? : String = "",
) -> Array[MutationCandidate] {
  discover_with_rules(source, rules_for_profile(profile), file~)
}

///|
pub fn validate_rules(rules : ArrayView[MutationRule]) -> RuleSetValidation {
  let duplicate_labels : Array[String] = []
  let empty_tokens : Array[String] = []
  for index, rule in rules {
    if rule.from == "" || rule.to == "" {
      empty_tokens.push(rule.label)
    }
    for other_index in (index + 1).. String {
  let selected = match profile {
    Some(value) => rule_catalog().filter(entry => entry.profile == value)
    None => rule_catalog()
  }
  let lines : Array[String] = [
    "Mutation rule catalog", "| Profile | Kind | Label | Change |", "| --- | --- | --- | --- |",
  ]
  for entry in selected {
    let rule = entry.rule
    lines.push(
      "| \{profile_label(entry.profile)} | \{mutation_kind_label(rule.kind)} | " +
      "\{rule.label} | `\{rule.from}` -> `\{rule.to}` |",
    )
  }
  lines.join("\n")
}

///|
fn basic_rule_description(label : String) -> String {
  match label {
    "eq-to-ne" => "turns equality checks into inequality checks"
    "ne-to-eq" => "turns inequality checks into equality checks"
    "and-to-or" => "weakens a conjunction into a disjunction"
    "or-to-and" => "strengthens a disjunction into a conjunction"
    "true-to-false" => "flips a true literal"
    "false-to-true" => "flips a false literal"
    _ => "operator replacement"
  }
}

///|
fn boundary_rule_description(label : String) -> String {
  match label {
    "zero-to-one" => "moves a zero boundary up by one"
    "one-to-zero" => "moves a one boundary down to zero"
    "minus-one-to-zero" => "moves a negative sentinel to zero"
    _ => "numeric boundary replacement"
  }
}

///|
fn experimental_rule_description(label : String) -> String {
  match label {
    "le-to-gt" | "lt-to-ge" | "ge-to-lt" | "gt-to-le" =>
      "inverts a relational boundary"
    "add-to-mul" | "sub-to-mul" | "mul-to-add" | "div-to-add" =>
      "replaces arithmetic with a stronger alternative"
    _ => "experimental operator replacement"
  }
}