use std::convert::TryInto;

use rand::rng;
use rand::seq::SliceRandom;
use serde_yaml::{Number, Value};

use crate::interpolator::INTERPOLATION_REGEX;

use crate::actions::Request;
use crate::benchmark::Benchmark;

pub fn is_that_you(item: &Value) -> bool {
  item.get("request").and_then(|v| v.as_mapping()).is_some() && item.get("with_items_range").and_then(|v| v.as_mapping()).is_some()
}

pub fn expand(item: &Value, benchmark: &mut Benchmark) {
  if let Some(with_iter_items) = item.get("with_items_range").and_then(|v| v.as_mapping()) {
    let lstart = Value::String("start".into());
    let lstep = Value::String("step".into());
    let lstop = Value::String("stop".into());

    let vstart = with_iter_items.get(&lstart).expect("Start property is mandatory");
    let default_step = Value::Number(Number::from(1));
    let vstep = with_iter_items.get(&lstep).unwrap_or(&default_step);
    let vstop = with_iter_items.get(&lstop).expect("Stop property is mandatory");

    let start: &str = vstart.as_str().unwrap_or("");
    let step: &str = vstep.as_str().unwrap_or("");
    let stop: &str = vstop.as_str().unwrap_or("");

    if INTERPOLATION_REGEX.is_match(start) {
      panic!("Interpolations not supported in 'start' property!");
    }

    if INTERPOLATION_REGEX.is_match(step) {
      panic!("Interpolations not supported in 'step' property!");
    }

    if INTERPOLATION_REGEX.is_match(stop) {
      panic!("Interpolations not supported in 'stop' property!");
    }

    let start: i64 = vstart.as_i64().expect("Start needs to be a number");
    let step: i64 = vstep.as_i64().expect("Step needs to be a number");
    let stop: i64 = vstop.as_i64().expect("Stop needs to be a number");

    let stop = stop + 1; // making stop inclusive

    if stop > start && start > 0 {
      let mut with_items: Vec<i64> = (start..stop).step_by(step as usize).collect();

      if let Some(true) = item.get("shuffle").and_then(|v| v.as_bool()) {
        let mut rng = rng();
        with_items.shuffle(&mut rng);
      }

      if let Some(pick) = item.get("pick").and_then(|v| v.as_i64()) {
        with_items.truncate(pick.try_into().expect("pick can't be larger than size of range"))
      }

      for (index, value) in with_items.iter().enumerate() {
        let index = index as u32;

        benchmark.push(Box::new(Request::new(item, Some(Value::Number(Number::from(*value))), Some(index))));
      }
    }
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn expand_multi_range() {
    let text = "---\nname: foobar\nrequest:\n  url: /api/{{ item }}\nwith_items_range:\n  start: 2\n  step: 2\n  stop: 20";
    let docs = crate::reader::read_file_as_yml_from_str(text);
    let doc = &docs[0];
    let mut benchmark: Benchmark = Benchmark::new();

    expand(doc, &mut benchmark);

    assert!(is_that_you(doc));
    assert_eq!(benchmark.len(), 10);
  }

  #[test]
  fn expand_multi_range_should_limit_requests_using_the_pick_option() {
    let text = "---\nname: foobar\nrequest:\n  url: /api/{{ item }}\npick: 3\nwith_items_range:\n  start: 2\n  step: 2\n  stop: 20";
    let docs = crate::reader::read_file_as_yml_from_str(text);
    let doc = &docs[0];
    let mut benchmark: Benchmark = Benchmark::new();

    expand(doc, &mut benchmark);

    assert!(is_that_you(doc));
    assert_eq!(benchmark.len(), 3);
  }

  #[test]
  #[should_panic]
  fn invalid_expand() {
    let text = "---\nname: foobar\nrequest:\n  url: /api/{{ item }}\nwith_items_range:\n  start: 1\n  step: 2\n  stop: foo";
    let docs = crate::reader::read_file_as_yml_from_str(text);
    let doc = &docs[0];
    let mut benchmark: Benchmark = Benchmark::new();

    expand(doc, &mut benchmark);
  }

  #[test]
  #[should_panic]
  fn runtime_expand() {
    let text = "---\nname: foobar\nrequest:\n  url: /api/{{ item }}\nwith_items_range:\n  start: 1\n  step: 2\n  stop: \"{{ memory }}\"";
    let docs = crate::reader::read_file_as_yml_from_str(text);
    let doc = &docs[0];
    let mut benchmark: Benchmark = Benchmark::new();

    expand(doc, &mut benchmark);
  }
}
