///|
pub enum ValidationSeverity {
  Pass
  Notice
  Failure
} derive(Debug, Eq)

///|
pub struct ValidationFinding {
  rule : String
  severity : ValidationSeverity
  message : String
} derive(Debug, Eq)

///|
pub struct ValidationReport {
  findings : Array[ValidationFinding]
  mut passed : Int
  mut notices : Int
  mut failures : Int
} derive(Debug)

///|
pub fn ValidationReport::new() -> ValidationReport {
  { findings: [], passed: 0, notices: 0, failures: 0 }
}

///|
pub fn ValidationReport::add(
  self : ValidationReport,
  finding : ValidationFinding,
) -> Unit {
  self.findings.push(finding)
  match finding.severity {
    Pass => self.passed = self.passed + 1
    Notice => self.notices = self.notices + 1
    Failure => self.failures = self.failures + 1
  }
}

///|
pub fn ValidationReport::ok(self : ValidationReport) -> Bool {
  self.failures == 0
}

///|
pub fn ValidationReport::finding_count(self : ValidationReport) -> Int {
  self.findings.length()
}

///|
pub fn ValidationReport::summary(self : ValidationReport) -> String {
  "passed=\{self.passed},notices=\{self.notices},failures=\{self.failures}"
}

///|
pub fn ValidationReport::to_text(self : ValidationReport) -> String {
  let mut output = self.summary() + "\n"
  for finding in self.findings {
    let level = match finding.severity {
      Pass => "PASS"
      Notice => "NOTICE"
      Failure => "FAIL"
    }
    output = output + "[\{level}] \{finding.rule}: \{finding.message}\n"
  }
  output
}

///|
fn finding(
  rule : String,
  severity : ValidationSeverity,
  message : String,
) -> ValidationFinding {
  { rule, severity, message }
}

///|
pub fn validate_config(config : EvaluationConfig) -> ValidationReport {
  let report = ValidationReport::new()
  if config.episodes > 0 {
    report.add(finding("episodes", Pass, "episode count is positive"))
  } else {
    report.add(
      finding("episodes", Notice, "empty runs are allowed for dry-run checks"),
    )
  }
  if config.max_steps > 0 {
    report.add(finding("max_steps", Pass, "episode budget is bounded"))
  } else {
    report.add(finding("max_steps", Failure, "episode budget must be positive"))
  }
  if config.seed > 0 {
    report.add(finding("seed", Pass, "seed is reproducible"))
  } else {
    report.add(finding("seed", Failure, "seed must be positive"))
  }
  if config.report_window <= config.max_steps {
    report.add(finding("window", Pass, "report window fits the step budget"))
  } else {
    report.add(
      finding("window", Notice, "report window exceeds one episode budget"),
    )
  }
  report
}

///|
pub fn validate_result(result : BenchmarkResult) -> ValidationReport {
  let report = ValidationReport::new()
  if result.episodes == result.rewards.length() {
    report.add(
      finding("reward_shape", Pass, "reward series matches episode count"),
    )
  } else {
    report.add(
      finding("reward_shape", Failure, "reward series length mismatch"),
    )
  }
  if result.episodes == result.steps.length() {
    report.add(finding("step_shape", Pass, "step series matches episode count"))
  } else {
    report.add(finding("step_shape", Failure, "step series length mismatch"))
  }
  if result.solve_rate() >= 0.0 && result.solve_rate() <= 1.0 {
    report.add(finding("solve_rate", Pass, "solve rate is normalized"))
  } else {
    report.add(finding("solve_rate", Failure, "solve rate is outside [0,1]"))
  }
  let reward_stats = result.reward_stats()
  if reward_stats.count == result.episodes {
    report.add(finding("reward_stats", Pass, "statistics cover all rewards"))
  } else {
    report.add(
      finding("reward_stats", Failure, "statistics do not cover all rewards"),
    )
  }
  report
}

///|
pub fn validate_gridworld(env : GridWorldEnv) -> ValidationReport {
  let report = ValidationReport::new()
  let states = env.state_space()
  let actions = env.actions()
  if states.length() == 16 {
    report.add(finding("grid_states", Pass, "4x4 state space is complete"))
  } else {
    report.add(
      finding("grid_states", Failure, "unexpected GridWorld state count"),
    )
  }
  if actions.length() == 4 {
    report.add(
      finding("grid_actions", Pass, "four directional actions are exposed"),
    )
  } else {
    report.add(finding("grid_actions", Failure, "unexpected action count"))
  }
  let start = env.reset()
  if start == 0 {
    report.add(
      finding("grid_reset", Pass, "reset returns the documented start state"),
    )
  } else {
    report.add(
      finding("grid_reset", Failure, "reset returned an unexpected state"),
    )
  }
  let transition = env.step(-1)
  if transition.next_state() == 0 {
    report.add(
      finding(
        "invalid_action",
        Pass,
        "invalid action is safely clamped to no movement",
      ),
    )
  } else {
    report.add(
      finding("invalid_action", Notice, "invalid action changes state"),
    )
  }
  report
}

///|
pub fn validate_cliff(env : CliffWalkingEnv) -> ValidationReport {
  let report = ValidationReport::new()
  if env.state_space().length() == 48 {
    report.add(finding("cliff_states", Pass, "12x4 state space is complete"))
  } else {
    report.add(
      finding("cliff_states", Failure, "unexpected CliffWalking state count"),
    )
  }
  if env.actions().length() == 4 {
    report.add(
      finding("cliff_actions", Pass, "four directional actions are exposed"),
    )
  } else {
    report.add(finding("cliff_actions", Failure, "unexpected action count"))
  }
  let start = env.reset()
  if start == 36 {
    report.add(
      finding("cliff_reset", Pass, "reset returns the documented start state"),
    )
  } else {
    report.add(
      finding("cliff_reset", Failure, "reset returned an unexpected state"),
    )
  }
  report
}

///|
pub fn validate_replay(buffer : ReplayBuffer) -> ValidationReport {
  let report = ValidationReport::new()
  if buffer.length() >= 0 {
    report.add(finding("replay_length", Pass, "buffer length is non-negative"))
  } else {
    report.add(finding("replay_length", Failure, "buffer length is invalid"))
  }
  if buffer.mean_reward() == buffer.mean_reward() {
    report.add(
      finding("replay_mean", Pass, "mean reward is finite for current inputs"),
    )
  } else {
    report.add(finding("replay_mean", Failure, "mean reward is not finite"))
  }
  let priorities = buffer.priorities()
  if priorities.length() == buffer.length() {
    report.add(
      finding(
        "replay_priority_shape",
        Pass,
        "priority series matches stored items",
      ),
    )
  } else {
    report.add(
      finding(
        "replay_priority_shape",
        Failure,
        "priority series length mismatch",
      ),
    )
  }
  report
}

///|
pub fn validate_policy(
  policy : Array[Int],
  action_count : Int,
) -> ValidationReport {
  let report = ValidationReport::new()
  let mut invalid = 0
  for action in policy {
    if action < 0 || action >= action_count {
      invalid = invalid + 1
    }
  }
  if invalid == 0 {
    report.add(
      finding("policy_range", Pass, "all actions belong to the action space"),
    )
  } else {
    report.add(
      finding("policy_range", Failure, "policy contains out-of-range actions"),
    )
  }
  let histogram = policy_histogram(policy, action_count)
  if stable_sum(histogram.map(fn(value) { value.to_double() })) ==
    policy.length().to_double() {
    report.add(
      finding("policy_histogram", Pass, "histogram accounts for every state"),
    )
  } else {
    report.add(
      finding("policy_histogram", Failure, "histogram lost policy entries"),
    )
  }
  report
}

///|
pub fn validate_schedule(
  schedule : EpsilonSchedule,
  samples : Int,
) -> ValidationReport {
  let report = ValidationReport::new()
  let values = schedule.values(samples)
  let mut invalid = 0
  for value in values {
    if value < 0.0 || value > 1.0 {
      invalid = invalid + 1
    }
  }
  if invalid == 0 {
    report.add(finding("epsilon_range", Pass, "epsilon remains within [0,1]"))
  } else {
    report.add(finding("epsilon_range", Failure, "epsilon left [0,1]"))
  }
  if values.length() == samples || (samples < 0 && values.length() == 0) {
    report.add(
      finding(
        "epsilon_shape",
        Pass,
        "schedule produced the requested sample shape",
      ),
    )
  } else {
    report.add(
      finding("epsilon_shape", Failure, "schedule sample shape is invalid"),
    )
  }
  report
}

///|
pub fn validate_planner(result : ValueIterationResult) -> ValidationReport {
  let report = ValidationReport::new()
  if result.values.length() == result.policy.length() {
    report.add(finding("planner_shape", Pass, "value and policy arrays align"))
  } else {
    report.add(
      finding("planner_shape", Failure, "value and policy arrays differ"),
    )
  }
  if result.iterations >= 0 {
    report.add(
      finding(
        "planner_iterations",
        Pass,
        "planner reports a valid iteration count",
      ),
    )
  } else {
    report.add(
      finding(
        "planner_iterations",
        Failure,
        "planner iteration count is invalid",
      ),
    )
  }
  if result.residual >= 0.0 {
    report.add(
      finding("planner_residual", Pass, "planner residual is non-negative"),
    )
  } else {
    report.add(
      finding("planner_residual", Failure, "planner residual is invalid"),
    )
  }
  report
}

///|
pub fn project_self_check(config : EvaluationConfig) -> String {
  let config_report = validate_config(config)
  let grid_report = validate_gridworld(GridWorldEnv::new())
  let cliff_report = validate_cliff(CliffWalkingEnv::new())
  let plan_report = validate_planner(
    gridworld_value_iteration(4, 4, 15, 0.95, 0.00001, 100),
  )
  "config: \{config_report.summary()}\ngrid: \{grid_report.summary()}\ncliff: \{cliff_report.summary()}\nplanner: \{plan_report.summary()}"
}

///|
pub fn source_scale_estimate() -> String {
  "core components=environment,policy,agents,trainer,metrics,benchmarks,planning,validation"
}

///|
pub fn acceptance_evidence() -> Array[String] {
  [
    "moon check", "moon test", "moon check --deny-warn", "moon test --deny-warn",
    "moon fmt && git diff --exit-code", "moon info && git diff --exit-code", "fixed-seed benchmark suite",
    "boundary and invalid-input tests",
  ]
}