///|
/// A public contract result for downstream packages and release scripts.
pub struct ContractResult {
  name : String
  passed : Bool
  detail : String
}

///|
fn contract(name : String, passed : Bool, detail : String) -> ContractResult {
  ContractResult::{ name, passed, detail }
}

///|
pub fn contract_reset(kind : ScenarioKind, seed : Int) -> ContractResult {
  let env = new(kind, seed)
  let first = env.reset()
  let second = env.reset()
  let passed = first.agent_x == second.agent_x &&
    first.agent_y == second.agent_y &&
    first.step_count == 0 &&
    !first.done
  contract(
    "reset",
    passed,
    if passed {
      "reset is deterministic"
    } else {
      "reset state changed unexpectedly"
    },
  )
}

///|
pub fn contract_step_progress(
  kind : ScenarioKind,
  seed : Int,
) -> ContractResult {
  let env = new(kind, seed)
  let before = env.reset()
  let result = env.step(Stay)
  let passed = result.observation.step_count == before.step_count + 1
  contract(
    "step-progress",
    passed,
    if passed {
      "step count advances exactly once"
    } else {
      "step count contract failed"
    },
  )
}

///|
pub fn contract_render(kind : ScenarioKind, seed : Int) -> ContractResult {
  let env = new(kind, seed)
  let render = env.reset().ascii
  let passed = render.contains(scenario_name(kind)) &&
    render.contains("G") &&
    render.length() > 0
  contract(
    "render",
    passed,
    if passed {
      "render contains scenario and goal"
    } else {
      "render is incomplete"
    },
  )
}

///|
pub fn contract_solver(kind : ScenarioKind, seed : Int) -> ContractResult {
  let env = new(kind, seed)
  let _ = env.reset()
  let plan = env.shortest_path()
  let passed = plan.found &&
    plan.steps == plan.actions.length() &&
    plan.steps > 0
  contract(
    "solver",
    passed,
    if passed {
      "planner returns a non-empty consistent route"
    } else {
      "planner route contract failed"
    },
  )
}

///|
pub fn contract_dataset(kind : ScenarioKind, seed : Int) -> ContractResult {
  let data = collect_episode(kind, seed, ShortestPath, 512)
  let passed = data.length() > 0 &&
    data.to_csv().contains("scenario,seed,index")
  contract(
    "dataset",
    passed,
    if passed {
      "trajectory has a CSV header and transitions"
    } else {
      "dataset export failed"
    },
  )
}

///|
pub fn contract_replay(kind : ScenarioKind, seed : Int) -> ContractResult {
  let result = replay_check(kind, seed, [Up, Down, Left, Right, Stay])
  let passed = result.mismatch_count == 0
  contract(
    "replay",
    passed,
    if passed {
      "identical seed and actions replay identically"
    } else {
      "replay mismatch detected"
    },
  )
}

///|
pub fn contracts_for(kind : ScenarioKind, seed : Int) -> Array[ContractResult] {
  [
    contract_reset(kind, seed),
    contract_step_progress(kind, seed),
    contract_render(kind, seed),
    contract_solver(kind, seed),
    contract_dataset(kind, seed),
    contract_replay(kind, seed),
  ]
}

///|
pub fn contracts_passed(kind : ScenarioKind, seed : Int) -> Bool {
  for item in contracts_for(kind, seed) {
    if !item.passed {
      return false
    }
  }
  true
}

///|
pub fn all_contracts_pass(seed : Int) -> Bool {
  for
    kind in [
      GridWorld,
      CliffWalking,
      Maze,
      FrozenLakeLike,
      RandomMaze,
      EmptyRoom,
      FourRooms,
    ] {
    if !contracts_passed(kind, seed) {
      return false
    }
  }
  true
}

///|
pub fn contracts_report(seed : Int) -> String {
  let builder = StringBuilder::new()
  for
    kind in [
      GridWorld,
      CliffWalking,
      Maze,
      FrozenLakeLike,
      RandomMaze,
      EmptyRoom,
      FourRooms,
    ] {
    builder.write_string(scenario_name(kind))
    builder.write_char(':')
    for item in contracts_for(kind, seed) {
      builder.write_char(' ')
      builder.write_string(item.name)
      builder.write_char('=')
      builder.write_object(item.passed)
    }
    builder.write_char('\n')
  }
  builder.to_string()
}

///|
/// Verify a plan remains valid when executed one action at a time.
pub fn plan_is_valid(kind : ScenarioKind, seed : Int) -> Bool {
  let env = new(kind, seed)
  let _ = env.reset()
  let plan = env.shortest_path()
  if !plan.found {
    false
  } else {
    let stochastic = match kind {
      FrozenLakeLike => true
      _ => false
    }
    if stochastic {
      true
    } else {
      let mut valid = true
      for action in plan.actions {
        if env.done {
          valid = false
        }
        let result = env.step(action)
        if result.truncated {
          valid = false
        }
      }
      valid && env.agent_x == env.goal_x && env.agent_y == env.goal_y
    }
  }
}

///|
pub fn all_plans_valid(seed : Int) -> Bool {
  for
    kind in [
      GridWorld,
      CliffWalking,
      Maze,
      FrozenLakeLike,
      RandomMaze,
      EmptyRoom,
      FourRooms,
    ] {
    if !plan_is_valid(kind, seed) {
      return false
    }
  }
  true
}

///|
pub fn project_status(seed : Int) -> String {
  let quality = quality_score(seed)
  let builder = StringBuilder::new()
  builder.write_string("quality=")
  builder.write_object(quality.passed)
  builder.write_string(" | contracts=")
  builder.write_object(all_contracts_pass(seed))
  builder.write_string(" | plans=")
  builder.write_object(all_plans_valid(seed))
  builder.to_string()
}