///|
pub(all) struct ScenarioAnalysis {
  name : String
  trace : CacheTrace
  valid : Bool
  errors : Array[String]
} derive(Eq, Debug)

///|
fn scenario_mode(value : String) -> CacheMode raise ScenarioError {
  match CacheMode::from_string(value) {
    Some(mode) => mode
    None => raise ScenarioError("mode must be private or shared")
  }
}

///|
fn scenario_action_matches(expected : String, actual : CacheActionKind) -> Bool {
  let value = expected
    .trim(chars=" \t")
    .to_lower()
    .replace_all(old="-", new="_")
  match actual {
    ServeFresh => value == "serve_fresh" || value == "fresh" || value == "hit"
    Revalidate => value == "revalidate"
    Fetch => value == "fetch" || value == "miss"
    ServeStale => value == "serve_stale" || value == "stale"
    Bypass => value == "bypass"
    OnlyIfCachedMiss =>
      value == "only_if_cached_miss" || value == "only_if_cached"
  }
}

///|
fn trace_for_scenario_miss(
  request : RequestMeta,
  policy : RequestPolicy,
) -> CacheTrace {
  let action = if policy.bypass {
    Bypass
  } else if policy.only_if_cached {
    OnlyIfCachedMiss
  } else {
    Fetch
  }
  let trace = CacheTrace::new(action)
  match primary_cache_key(request) {
    Some(key) => trace.primary_key = Some(key.label())
    None => ()
  }
  match action {
    Bypass => {
      trace.add_reason(CacheReason::new(RequestNoStore))
      trace.add_reason(CacheReason::new(RuntimeBypass))
    }
    OnlyIfCachedMiss => {
      trace.add_reason(CacheReason::new(RequestOnlyIfCached))
      trace.add_reason(CacheReason::new(RuntimeOnlyIfCachedMiss))
    }
    Fetch => {
      trace.add_reason(CacheReason::new(RuntimeMiss))
      trace.add_reason(CacheReason::new(RuntimeFetch))
    }
    _ => ()
  }
  trace
}

///|
fn trace_for_stored_scenario(
  scenario : CacheScenario,
  request : RequestMeta,
  response_data : ScenarioResponse,
  options : CacheOptions,
) -> CacheTrace {
  let response = response_data.to_meta(request.request_time)
  let decision = evaluate_cached_response(
    request,
    response,
    options,
    Timestamp::from_seconds(scenario.now.to_int64()),
  )
  let trace = decision.trace
  match primary_cache_key(request) {
    Some(key) => trace.primary_key = Some(key.label())
    None => ()
  }
  match build_variant_key(request.headers, parse_vary(response.headers)) {
    Some(variant) => trace.selected_variant = Some(variant.label)
    None => ()
  }
  match response_validator_kind(response) {
    Some(kind) => trace.validator = Some(kind.label())
    None => ()
  }
  trace
}

///|
pub fn analyze_scenario(
  scenario : CacheScenario,
) -> ScenarioAnalysis raise ScenarioError {
  let mode = scenario_mode(scenario.mode)
  let options = if mode is Shared {
    CacheOptions::shared_cache()
  } else {
    CacheOptions::private_cache()
  }
  let request = scenario.request.to_meta()
  let trace = match scenario.stored_response {
    Some(response) =>
      trace_for_stored_scenario(scenario, request, response, options)
    None => trace_for_scenario_miss(request, evaluate_request_policy(request))
  }
  let errors : Array[String] = []
  if scenario.name.trim().is_empty() {
    errors.push("name must not be empty")
  }
  if normalize_cache_uri(request.uri) is None {
    errors.push("request.uri must be an absolute HTTP(S) URI")
  }
  match scenario.stored_response {
    Some(response) =>
      if response.status < 100 || response.status > 599 {
        errors.push("stored_response.status must be between 100 and 599")
      }
    None => ()
  }
  match scenario.expected {
    Some(expected) => {
      if !scenario_action_matches(expected.action, trace.action) {
        errors.push(
          "expected action \{expected.action}, got \{trace.action.label()}",
        )
      }
      match expected.reason {
        Some(code) =>
          if !trace.reasons.any(fn(reason) { reason.code.code() == code }) {
            errors.push("expected reason \{code} was not emitted")
          }
        None => ()
      }
    }
    None => ()
  }
  ScenarioAnalysis::{
    name: scenario.name,
    trace,
    valid: errors.length() == 0,
    errors,
  }
}

///|
pub fn analyze_scenario_json(
  text : String,
) -> ScenarioAnalysis raise ScenarioError {
  analyze_scenario(parse_scenario(text))
}

///|
pub fn explain_scenario_text(text : String) -> String raise ScenarioError {
  let analysis = analyze_scenario_json(text)
  let report = analysis.trace.text_report()
  if analysis.errors.length() == 0 {
    report
  } else {
    let errors = analysis.errors.join("\n- ")
    "\{report}\nValidation errors:\n- \{errors}"
  }
}

///|
pub fn explain_scenario_json(text : String) -> String raise ScenarioError {
  analyze_scenario_json(text).trace.json_report()
}

///|
pub fn validate_scenario_text(text : String) -> String raise ScenarioError {
  let analysis = analyze_scenario_json(text)
  if analysis.valid {
    "VALID: \{analysis.name}"
  } else {
    let errors = analysis.errors.join("\n- ")
    "INVALID: \{analysis.name}\n- \{errors}"
  }
}