///|
/// The result of a shrink search: how many shrinks succeeded, how
/// many were tried unsuccessfully, and the final minimal result.
priv struct ShrinkResult {
  num_shrinks : Int32
  num_shrink_tries : Int32
  num_shrink_final : Int32
  result : @state.SingleResult
}

///|
fn validate_config(
  max_size : Int32,
  max_success : Int32,
  max_shrinks : Int32,
) -> Unit raise Failure {
  if max_size <= 0 {
    raise Failure("quickcheck: max_size must be greater than 0")
  }
  if max_success < 0 {
    raise Failure("quickcheck: max_success must be greater than or equal to 0")
  }
  if max_shrinks < 0 {
    raise Failure("quickcheck: max_shrinks must be greater than or equal to 0")
  }
}

///|
fn finish_report(
  report : @report.CheckReport,
  verbose : Bool,
) -> Unit raise Failure {
  let rendered = report.render(verbose~)
  if report.is_ok() {
    println(rendered)
  } else {
    fail(rendered)
  }
}

///|
/// Run a `Testable` property against randomly-generated inputs and
/// print the outcome. The driver runs up to `max_success` successful
/// tests, trying increasingly large inputs up to `max_size`, and
/// discarding no more than `max_success × discard_ratio` inputs
/// before giving up. On failure it shrinks for up to `max_shrinks`
/// steps before reporting. `seed` initializes the deterministic PRNG
/// and is retained in the report; verbose failure output includes it.
///
/// `expect` asserts the expected outcome (`Success` by default); the
/// driver flips the verdict accordingly (a failing property with
/// `expect=Fail` is a pass). `abort=true` short-circuits the outer
/// loop after the first test result is processed — note that a
/// failing result still goes through the full shrink search before
/// returning.
///
/// Raises `Failure` on mismatch with `expect` or when a configured
/// limit is invalid — so it's the drop-in entry point inside
/// `test "…"` blocks.
pub fn[P : Testable] quick_check(
  prop : P,
  max_shrinks~ : Int32 = 100,
  max_success~ : Int32 = 100,
  max_size~ : Int32 = 100,
  discard_ratio~ : Int32 = 10,
  seed~ : UInt64 = 37,
  expect~ : Expected = Success,
  abort~ : Bool = false,
  verbose~ : Bool = false,
) -> Unit raise Failure {
  validate_config(max_size, max_success, max_shrinks)
  let prop = @property.with_run_options(prop, expect, abort)
  let state = @state.from_config({
    max_shrinks,
    max_success,
    max_size,
    max_discard_ratio: discard_ratio,
    seed,
  })
  state.expected = expect
  let report = state.run_test(prop.property())
  finish_report(report, verbose)
}

///|
/// Like `quick_check` but returns the formatted property outcome as a
/// `String` instead of printing or raising it. Invalid configured
/// limits still raise `Failure`. Useful in tests that want to pin the
/// driver's output with `inspect(..., content=...)`.
pub fn[P : Testable] quick_check_silence(
  prop : P,
  max_shrinks~ : Int32 = 100,
  max_success~ : Int32 = 100,
  max_size~ : Int32 = 100,
  discard_ratio~ : Int32 = 10,
  seed~ : UInt64 = 37,
  expect~ : Expected = Success,
  abort~ : Bool = false,
  verbose~ : Bool = false,
) -> String raise Failure {
  validate_config(max_size, max_success, max_shrinks)
  let prop = @property.with_run_options(prop, expect, abort)
  let state = @state.from_config({
    max_shrinks,
    max_success,
    max_size,
    max_discard_ratio: discard_ratio,
    seed,
  })
  state.expected = expect
  state.run_test(prop.property()).render(verbose~)
}

///|
/// Thin sugar over `quick_check`: accept a plain `(A) -> B` property
/// function, wrap it in `Arrow`, and run. The shortest path from a
/// user-written property to a full QuickCheck run.
pub fn[A : @coreqc.Arbitrary + @shrink.Shrink + Debug, B : Testable] quick_check_fn(
  f : (A) -> B,
  max_shrinks? : Int32,
  max_success? : Int32,
  max_size? : Int32,
  discard_ratio? : Int32,
  seed~ : UInt64 = 37,
  expect~ : Expected = Success,
  abort~ : Bool = false,
  verbose~ : Bool = false,
) -> Unit raise Failure {
  quick_check(
    Arrow(f),
    max_shrinks?,
    max_success?,
    max_size?,
    discard_ratio?,
    seed~,
    expect~,
    abort~,
    verbose~,
  )
}

///|
/// `quick_check_fn` variant that returns the formatted property
/// outcome as a `String` instead of raising / printing it. Invalid
/// configured limits still raise `Failure`.
pub fn[A : @coreqc.Arbitrary + @shrink.Shrink + Debug, B : Testable] quick_check_fn_silence(
  f : (A) -> B,
  max_shrinks? : Int32,
  max_success? : Int32,
  max_size? : Int32,
  discard_ratio? : Int32,
  seed~ : UInt64 = 37,
  expect~ : Expected = Success,
  abort~ : Bool = false,
  verbose~ : Bool = false,
) -> String raise Failure {
  quick_check_silence(
    Arrow(f),
    max_shrinks?,
    max_success?,
    max_size?,
    discard_ratio?,
    seed~,
    expect~,
    abort~,
    verbose~,
  )
}

///|
/// `quick_check_fn` for properties that may `raise`. A raised error
/// is treated as a counter-example with the error captured as the
/// reason.
pub fn[A : @coreqc.Arbitrary + @shrink.Shrink + Debug, B : Testable] quick_check_fn_error(
  f : (A) -> B raise,
  max_shrinks? : Int32,
  max_success? : Int32,
  max_size? : Int32,
  discard_ratio? : Int32,
  seed~ : UInt64 = 37,
  expect~ : Expected = Success,
  abort~ : Bool = false,
  verbose~ : Bool = false,
) -> Unit raise Failure {
  quick_check(
    ArrowError(f),
    max_shrinks?,
    max_success?,
    max_size?,
    discard_ratio?,
    seed~,
    expect~,
    abort~,
    verbose~,
  )
}

///|
/// `quick_check_fn_error` silent variant that returns the formatted
/// property outcome as a `String`. Invalid configured limits still
/// raise `Failure`.
pub fn[A : @coreqc.Arbitrary + @shrink.Shrink + Debug, B : Testable] quick_check_fn_error_silence(
  f : (A) -> B raise,
  max_shrinks? : Int32,
  max_success? : Int32,
  max_size? : Int32,
  discard_ratio? : Int32,
  seed~ : UInt64 = 37,
  expect~ : Expected = Success,
  abort~ : Bool = false,
  verbose~ : Bool = false,
) -> String raise Failure {
  quick_check_silence(
    ArrowError(f),
    max_shrinks?,
    max_success?,
    max_size?,
    discard_ratio?,
    seed~,
    expect~,
    abort~,
    verbose~,
  )
}

///|
fn active_classes(
  classes : @list.List[(String, Bool)],
) -> @sorted_set.SortedSet[String] {
  [ for pair in classes if pair.1 => pair.0 ] |> @sorted_set.from_array
}

///|
/// Wrap up a finished test run — produce the formatted success
/// report when the expected outcome matches, or raise the
/// appropriate `TestError` when it does not (e.g. the property was
/// supposed to fail but no counter-example appeared).
fn @state.State::complete_test(
  self : @state.State,
  _prop : Property,
) -> @report.TestSuccess raise @report.TestError {
  if self.expected is Fail {
    raise NoneExpectedFail(
      num_tests=self.num_success_tests,
      num_discarded=self.num_discarded_tests,
      coverage=self.collects,
      seed=self.seed,
      output="*** \{self.counts()} Failed! Expected failure, but passed!",
    )
  } else {
    Success(
      num_tests=self.num_success_tests,
      coverage=self.collects,
      output="+++ \{self.counts()} Ok, passed!",
    )
  }
}

///|
/// Abandon the run when too many inputs have been discarded (the
/// `max_success × discard_ratio` budget is exhausted). Produces the
/// `GaveUp` outcome with the current coverage snapshot.
fn @state.State::give_up(
  self : @state.State,
  _prop : Property,
) -> @report.TestSuccess raise @report.TestError {
  if self.expected is GaveUp {
    Success(
      num_tests=self.num_success_tests,
      coverage=self.collects,
      output="+++ \{self.counts()} Ok, gave up!",
    )
  } else {
    raise GaveUp(
      num_tests=self.num_success_tests,
      num_discarded=self.num_discarded_tests,
      coverage=self.collects,
      seed=self.seed,
      output="*** \{self.counts()} Gave up! Passed only \{self.num_success_tests} tests.",
    )
  }
}

///|
/// Enter the shrink loop on a counter-example: starting from `res`
/// and the iter of alternative `Rose` sub-trees, walk the shrinkers
/// to find a minimal failing case, then raise the appropriate
/// `TestError` (or `TestSuccess` if the failure was expected).
fn @state.State::find_failure(
  self : @state.State,
  rose : @rose.Rose[@state.SingleResult],
) -> @report.TestSuccess raise @report.TestError {
  let sr = { ..self, num_try_shrinks: 0, }.local_min(rose)
  self.callback_post_final_failure(sr.result)
  match rose.val.expect {
    Success | GaveUp =>
      raise Fail(
        error=rose.val.error,
        num_tests=self.num_success_tests,
        num_discarded=self.num_discarded_tests,
        num_shrinks=sr.num_shrinks,
        num_shrink_tries=sr.num_shrink_tries,
        num_shrink_final=sr.num_shrink_final,
        reason=rose.val.reason,
        output="*** \{self.counts()} Failed! \{rose.val.reason}",
        coverage=self.collects,
        failing_case=sr.result.test_case.to_array(),
        failing_labels=sr.result.labels.to_array(),
        failing_classes=active_classes(sr.result.classes),
        seed=self.seed,
      )
    Fail =>
      Success(
        num_tests=self.num_success_tests,
        coverage=self.collects,
        output="+++ \{self.counts()} Ok! Failed as expected.",
      )
  }
}

///|
/// The inner shrink-search loop: given a failing `Rose` tree,
/// walk downward as long as we keep finding smaller failures,
/// bailing out after `self.max_shrinks_` total attempts. Returns
/// the full tally of shrink counts and the final minimal result.
fn @state.State::local_min(
  self : @state.State,
  rose : @rose.Rose[@state.SingleResult],
) -> ShrinkResult {
  for res = rose.val, ts = rose.branch {
    if self.num_success_shrinks + self.num_to_try_shrinks >= self.max_shrinks_ {
      break {
        num_shrinks: self.num_success_shrinks,
        num_shrink_tries: self.num_to_try_shrinks - self.num_try_shrinks,
        num_shrink_final: self.num_try_shrinks,
        result: res,
      }
    }
    match ts.head() {
      None =>
        break {
          num_shrinks: self.num_success_shrinks,
          num_shrink_tries: self.num_to_try_shrinks - self.num_try_shrinks,
          num_shrink_final: self.num_try_shrinks,
          result: res,
        }
      Some({ val, branch, }) => {
        self.callback_post_test(val)
        match val.status {
          Failed => {
            self.num_success_shrinks += 1
            self.num_try_shrinks = 0
            continue val, branch
          }
          Passed | Rejected => {
            self.num_to_try_shrinks += 1
            self.num_try_shrinks += 1
            continue res, ts.drop(1)
          }
        }
      }
    }
  }
}