///|
fn attribute_text(
  owner : Schema,
  attrs : Array[Int32],
) -> String raise DesignError {
  "{" + owner.names(attrs).join(", ") + "}"
}

///|
fn dependency_text(owner : Schema, fd : FD) -> String raise DesignError {
  attribute_text(owner, fd.left) + " -> " + attribute_text(owner, fd.right)
}

///|
fn form_text(form : NormalForm) -> String {
  match form {
    Second => "2NF"
    Third => "3NF"
    BoyceCodd => "BCNF"
  }
}

///|
pub fn Schema::plan_report(
  self : Schema,
  plan : Plan,
  limits~ : Limits = default_limits(),
) -> String raise DesignError {
  let parts = self.fragments(plan.relations, true)
  let audit = self.audit_plan(plan, limits~)
  let locality = self.dependency_locality(parts, limits~)
  let out = StringBuilder()
  out.write_string("Target: " + form_text(plan.target) + "\n")
  out.write_string(
    "Lossless under declared FDs: " + audit.lossless.to_string() + "\n",
  )
  out.write_string(
    "Dependency preserving: " + audit.dependency_preserving.to_string() + "\n",
  )
  out.write_string(
    "Target satisfied: " + audit.target_satisfied.to_string() + "\n",
  )
  for i in 0.. String raise DesignError {
  let audit = self.audit_dependencies(owner, limits~)
  let profiles = self.column_profiles(limits~)
  let fds = owner.dependencies()
  let out = StringBuilder()
  out.write_string(
    "Set-valued observed rows: " + self.row_count().to_string() + "\n",
  )
  out.write_string(
    "Checked declared dependencies: " +
    audit.checked_dependencies.length().to_string() +
    "\n",
  )
  out.write_string(
    "Skipped dependencies lacking columns: " +
    audit.skipped_dependencies.length().to_string() +
    "\n",
  )
  for witness in audit.violations {
    out.write_string(
      "Rows " +
      witness.first_row.to_string() +
      " and " +
      witness.second_row.to_string() +
      " violate " +
      dependency_text(owner, fds[witness.dependency_index]) +
      "; differ on " +
      attribute_text(owner, witness.differing_attributes) +
      "\n",
    )
  }
  for profile in profiles {
    out.write_string(
      "Column " +
      owner.labels[profile.attribute] +
      ": distinct=" +
      profile.distinct_values.to_string() +
      ", empty=" +
      profile.empty_values.to_string() +
      ", max-code-units=" +
      profile.maximum_code_units.to_string() +
      "\n",
    )
  }
  out.write_string(
    "Row indices refer to deduplicated input order; empty strings are ordinary values.\n",
  )
  out.write_string(
    "No violations observed does not establish a universal functional dependency.\n",
  )
  out.to_string()
}

///|
pub fn MigrationReview::summary(
  self : MigrationReview,
  owner : Schema,
) -> String raise DesignError {
  let out = StringBuilder()
  out.write_string(
    "Lossless regression: " + self.lossless_regression.to_string() + "\n",
  )
  out.write_string(
    "Normal-form regression: " + self.normal_form_regression.to_string() + "\n",
  )
  for fd in self.lost_dependencies {
    out.write_string("Lost preservation: " + dependency_text(owner, fd) + "\n")
  }
  for fd in self.gained_dependencies {
    out.write_string(
      "Gained preservation: " + dependency_text(owner, fd) + "\n",
    )
  }
  for relation in self.layout.removed {
    out.write_string("Remove: " + attribute_text(owner, relation) + "\n")
  }
  for relation in self.layout.added {
    out.write_string("Add: " + attribute_text(owner, relation) + "\n")
  }
  out.write_string(
    "Review aid only; does not authorize or execute a database migration.\n",
  )
  out.to_string()
}