use std::collections::{HashMap, HashSet};
use std::fmt;
use std::fs;
use std::path::PathBuf;

use http::Method;

use crate::parser::context;

use super::{assertion::Guard, Assertion, FormDataType, MapIteration, Request, ResponseCapture};

pub const MAX_ARRAY_VARIABLES_PER_REQUEST: usize = 2;
pub const MAX_MAP_COMBINATIONS: usize = 128;

#[derive(Debug, Clone, Default)]
pub struct VariableStore {
    scalars: HashMap<String, String>,
    arrays: HashMap<String, Vec<String>>,
}

impl VariableStore {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn scalars(&self) -> &HashMap<String, String> {
        &self.scalars
    }

    pub fn set_scalar(&mut self, key: String, value: String) {
        self.arrays.remove(&key);
        self.scalars.insert(key, value);
    }

    pub fn set_array(&mut self, key: String, values: Vec<String>) {
        self.scalars.remove(&key);
        self.arrays.insert(key, values);
    }

    pub fn contains_array(&self, key: &str) -> bool {
        self.arrays.contains_key(key)
    }

    pub fn get_array(&self, key: &str) -> Option<&[String]> {
        self.arrays.get(key).map(|vals| vals.as_slice())
    }

    pub fn clone_scalars(&self) -> HashMap<String, String> {
        self.scalars.clone()
    }
}

#[derive(Debug, Clone)]
pub struct RequestTemplate {
    pub group_key: String,
    pub description_template: String,
    pub method: Method,
    pub url: String,
    pub headers: HashMap<String, String>,
    pub query_params: HashMap<String, String>,
    pub form_text: HashMap<String, String>,
    pub form_files: HashMap<String, String>,
    pub body: Option<String>,
    pub body_content_type: Option<String>,
    pub callback_src: Vec<String>,
    pub response_captures: Vec<ResponseCapture>,
    pub assertions: Vec<Assertion>,
    pub fragment_includes: Vec<FragmentInclude>,
    pub declared_dependencies: Vec<String>,
    pub variable_store: VariableStore,
    pub working_dir: PathBuf,
}

#[derive(Debug, Clone)]
pub struct FragmentInclude {
    pub path: String,
    pub guard: Option<String>,
    pub assertion_index: usize,
}

#[derive(Debug)]
pub enum TemplateError {
    TooManyArrayVariables {
        request: String,
        limit: usize,
        found: usize,
    },
    MissingArrayValues {
        request: String,
        variable: String,
    },
    EmptyArrayValues {
        request: String,
        variable: String,
    },
    TooManyCombinations {
        request: String,
        limit: usize,
        count: usize,
    },
    InvalidArrayValue {
        request: String,
        variable: String,
        value: String,
    },
    DependencyOnMappedRequest {
        request: String,
        dependency: String,
    },
    UnknownDependency {
        request: String,
        dependency: String,
    },
    InvalidFragmentGuard {
        request: String,
        guard: String,
        reason: String,
    },
    FragmentIo {
        request: String,
        path: String,
        reason: String,
    },
    FragmentParse {
        request: String,
        path: String,
        reason: String,
    },
    FragmentUnsupportedLine {
        request: String,
        path: String,
        line: String,
    },
    PromptInput {
        request: String,
        prompt: String,
        default: Option<String>,
    },
}

impl fmt::Display for TemplateError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            TemplateError::TooManyArrayVariables {
                request,
                limit,
                found,
            } => {
                write!(
                    f,
                    "Request '{}' references {} array variables but the limit is {}.",
                    request, found, limit
                )
            }
            TemplateError::MissingArrayValues { request, variable } => {
                write!(
                    f,
                    "Request '{}' references array variable '{}' which is not defined as an array.",
                    request, variable
                )
            }
            TemplateError::EmptyArrayValues { request, variable } => {
                write!(
                    f,
                    "Request '{}' references array variable '{}' but it contains no values.",
                    request, variable
                )
            }
            TemplateError::TooManyCombinations {
                request,
                limit,
                count,
            } => {
                write!(
                    f,
                    "Request '{}' expands into {} combinations which exceeds the limit of {}.",
                    request, count, limit
                )
            }
            TemplateError::InvalidArrayValue {
                request,
                variable,
                value,
            } => {
                write!(
                    f,
                    "Request '{}' defines array variable '{}' with unsupported value '{}'.",
                    request, variable, value
                )
            }
            TemplateError::DependencyOnMappedRequest {
                request,
                dependency,
            } => {
                write!(
					f,
					"Request '{}' depends on '{}' which expands into multiple iterations. Dependencies on mapped requests are not supported yet.",
					request, dependency
				)
            }
            TemplateError::UnknownDependency {
                request,
                dependency,
            } => {
                write!(
                    f,
                    "Request '{}' declares unknown dependency '{}'.",
                    request, dependency
                )
            }
            TemplateError::InvalidFragmentGuard {
                request,
                guard,
                reason,
            } => {
                write!(
                    f,
                    "Request '{}' has invalid fragment guard '[{}]': {}.",
                    request, guard, reason
                )
            }
            TemplateError::FragmentIo {
                request,
                path,
                reason,
            } => {
                write!(
                    f,
                    "Request '{}' failed to load fragment '{}': {}.",
                    request, path, reason
                )
            }
            TemplateError::FragmentParse {
                request,
                path,
                reason,
            } => {
                write!(
                    f,
                    "Request '{}' failed to parse fragment '{}': {}.",
                    request, path, reason
                )
            }
            TemplateError::FragmentUnsupportedLine {
                request,
                path,
                line,
            } => {
                write!(
                    f,
                    "Request '{}' cannot include line '{}' from fragment '{}'.",
                    request, line, path
                )
            }
            TemplateError::PromptInput {
                request,
                prompt,
                default,
            } => match default {
                Some(default) => write!(
                    f,
                    "Request '{}' requires prompt '{}' but no input was provided (default: {}).",
                    request, prompt, default
                ),
                None => write!(
                    f,
                    "Request '{}' requires prompt '{}' but no input was provided.",
                    request, prompt
                ),
            },
        }
    }
}

impl std::error::Error for TemplateError {}

pub fn expand_templates(templates: Vec<RequestTemplate>) -> Result<Vec<Request>, TemplateError> {
    let mut requests: Vec<Request> = Vec::new();

    for template in templates {
        let mut expanded = expand_template(&template)?;
        requests.append(&mut expanded);
    }

    resolve_dependencies(&mut requests)?;

    Ok(requests)
}

fn expand_template(template: &RequestTemplate) -> Result<Vec<Request>, TemplateError> {
    let array_vars = detect_array_variables(template);

    if array_vars.is_empty() {
        let request = build_request(template, &[], &[])?;
        return Ok(vec![request]);
    }

    if array_vars.len() > MAX_ARRAY_VARIABLES_PER_REQUEST {
        return Err(TemplateError::TooManyArrayVariables {
            request: template.group_key.clone(),
            limit: MAX_ARRAY_VARIABLES_PER_REQUEST,
            found: array_vars.len(),
        });
    }

    let mut arrays: Vec<(String, Vec<String>)> = Vec::new();
    for var in &array_vars {
        match template.variable_store.get_array(var) {
            Some(values) => {
                if values.is_empty() {
                    return Err(TemplateError::EmptyArrayValues {
                        request: template.group_key.clone(),
                        variable: var.clone(),
                    });
                }
                arrays.push((var.clone(), values.to_vec()));
            }
            None => {
                return Err(TemplateError::MissingArrayValues {
                    request: template.group_key.clone(),
                    variable: var.clone(),
                });
            }
        }
    }

    arrays.sort_by(|a, b| a.0.cmp(&b.0));

    let combination_count: usize = arrays.iter().map(|(_, values)| values.len()).product();

    if combination_count > MAX_MAP_COMBINATIONS {
        return Err(TemplateError::TooManyCombinations {
            request: template.group_key.clone(),
            limit: MAX_MAP_COMBINATIONS,
            count: combination_count,
        });
    }

    let mut requests = Vec::with_capacity(combination_count.max(1));
    let mut indices = vec![0usize; arrays.len()];

    loop {
        let request = build_request(template, &arrays, &indices)?;
        requests.push(request);

        if arrays.is_empty() {
            break;
        }

        let mut pos = arrays.len();
        let mut advanced = false;

        while pos > 0 {
            pos -= 1;
            if indices[pos] + 1 < arrays[pos].1.len() {
                indices[pos] += 1;
                for idx in indices.iter_mut().skip(pos + 1) {
                    *idx = 0;
                }
                advanced = true;
                break;
            }
        }

        if !advanced {
            break;
        }
    }

    Ok(requests)
}

fn build_request(
    template: &RequestTemplate,
    arrays: &[(String, Vec<String>)],
    indices: &[usize],
) -> Result<Request, TemplateError> {
    let (iteration_context, map_iteration) = build_iteration_context(template, arrays, indices)?;

    let resolved_description =
        context::resolve_with_context(template.description_template.as_str(), &iteration_context);

    let base_description = template.group_key.clone();
    let description = match &map_iteration {
        Some(iter) => {
            let suffix = iter.suffix();
            if resolved_description.trim().is_empty() {
                format!("{} {}", base_description, suffix)
                    .trim()
                    .to_string()
            } else {
                format!("{} {}", resolved_description, suffix)
                    .trim()
                    .to_string()
            }
        }
        None => {
            if resolved_description.trim().is_empty() {
                base_description.clone()
            } else {
                resolved_description
            }
        }
    };

    let resolved_url = context::resolve_with_context(template.url.as_str(), &iteration_context);

    let mut headers = HashMap::new();
    for (key, value) in &template.headers {
        headers.insert(
            key.clone(),
            context::resolve_with_context(value.as_str(), &iteration_context),
        );
    }

    let mut query_params = HashMap::new();
    for (key, value) in &template.query_params {
        query_params.insert(
            key.clone(),
            context::resolve_with_context(value.as_str(), &iteration_context),
        );
    }

    let mut form_data = HashMap::new();
    for (key, value) in &template.form_text {
        form_data.insert(
            key.clone(),
            FormDataType::Text(context::resolve_with_context(
                value.as_str(),
                &iteration_context,
            )),
        );
    }

    for (key, value) in &template.form_files {
        let resolved_path = context::resolve_with_context(value.as_str(), &iteration_context);
        let path = PathBuf::from(&resolved_path);
        let absolute = if path.is_absolute() {
            path
        } else {
            template.working_dir.join(path)
        };
        form_data.insert(key.clone(), FormDataType::File(absolute));
    }

    let body = template
        .body
        .as_ref()
        .map(|body| context::resolve_with_context(body.as_str(), &iteration_context));

    let body_content_type = template
        .body_content_type
        .as_ref()
        .map(|ctype| context::resolve_with_context(ctype.as_str(), &iteration_context));

    let callback_src = template
        .callback_src
        .iter()
        .map(|src| context::resolve_with_context(src.as_str(), &iteration_context))
        .collect();

    let mut request = Request {
        description,
        base_description,
        method: template.method.clone(),
        url: resolved_url,
        headers,
        query_params,
        form_data,
        body,
        body_content_type,
        callback_src,
        response_captures: template.response_captures.clone(),
        assertions: template.assertions.clone(),
        declared_dependencies: template.declared_dependencies.clone(),
        dependencies: template.declared_dependencies.clone(),
        context: iteration_context,
        working_dir: template.working_dir.clone(),
        map_iteration,
    };

    apply_fragment_includes(template, &mut request)?;

    Ok(request)
}

fn apply_fragment_includes(
    template: &RequestTemplate,
    request: &mut Request,
) -> Result<(), TemplateError> {
    if template.fragment_includes.is_empty() {
        return Ok(());
    }

    let mut assertion_offset = 0usize;

    for include in &template.fragment_includes {
        let guard_allows = match &include.guard {
            Some(expr) => {
                let guard = Guard::parse(expr.as_str(), &request.context).map_err(|err| {
                    TemplateError::InvalidFragmentGuard {
                        request: template.group_key.clone(),
                        guard: expr.clone(),
                        reason: err.0,
                    }
                })?;

                guard.evaluate_with_env(&request.context).map_err(|err| {
                    TemplateError::InvalidFragmentGuard {
                        request: template.group_key.clone(),
                        guard: expr.clone(),
                        reason: err.0,
                    }
                })?
            }
            None => true,
        };

        if !guard_allows {
            continue;
        }

        let resolved_path = context::resolve_with_context(include.path.as_str(), &request.context);
        let path = PathBuf::from(&resolved_path);
        let absolute = if path.is_absolute() {
            path
        } else {
            template.working_dir.join(&path)
        };

        let fragment_dir = absolute
            .parent()
            .map(|p| p.to_path_buf())
            .unwrap_or_else(|| template.working_dir.clone());

        let content = fs::read_to_string(&absolute).map_err(|err| TemplateError::FragmentIo {
            request: template.group_key.clone(),
            path: absolute.display().to_string(),
            reason: err.to_string(),
        })?;

        let preprocessed = crate::parser::preprocess_only(content.as_str(), &fragment_dir)
            .map_err(|err| TemplateError::FragmentParse {
                request: template.group_key.clone(),
                path: absolute.display().to_string(),
                reason: err,
            })?;

        let mut parsed: Vec<Assertion> = Vec::new();

        for line in preprocessed.lines() {
            let trimmed = line.trim();
            if trimmed.is_empty() || trimmed.starts_with('#') {
                continue;
            }

            if !trimmed.starts_with('^') && !trimmed.starts_with('[') {
                return Err(TemplateError::FragmentUnsupportedLine {
                    request: template.group_key.clone(),
                    path: absolute.display().to_string(),
                    line: trimmed.to_string(),
                });
            }

            let assertion = Assertion::parse(trimmed, &request.context).map_err(|err| {
                TemplateError::FragmentParse {
                    request: template.group_key.clone(),
                    path: absolute.display().to_string(),
                    reason: err.0,
                }
            })?;
            parsed.push(assertion);
        }

        if parsed.is_empty() {
            continue;
        }

        let insert_at = (include.assertion_index + assertion_offset).min(request.assertions.len());
        assertion_offset += parsed.len();

        request
            .assertions
            .splice(insert_at..insert_at, parsed.into_iter());
    }

    Ok(())
}

fn build_iteration_context(
    template: &RequestTemplate,
    arrays: &[(String, Vec<String>)],
    indices: &[usize],
) -> Result<(HashMap<String, String>, Option<MapIteration>), TemplateError> {
    let mut context = template.variable_store.clone_scalars();
    let mut iteration_pairs: Vec<(String, String)> = Vec::new();

    for (idx, (name, values)) in arrays.iter().enumerate() {
        let selected =
            values
                .get(indices[idx])
                .cloned()
                .ok_or_else(|| TemplateError::MissingArrayValues {
                    request: template.group_key.clone(),
                    variable: name.clone(),
                })?;

        let resolved = context::resolve_with_context(selected.as_str(), &context);
        validate_iteration_value(template, name, &resolved)?;
        context.insert(name.clone(), resolved.clone());
        iteration_pairs.push((name.clone(), resolved));
    }

    // Re-resolve scalar variables now that arrays are in scope.
    let scalar_keys: Vec<String> = template.variable_store.scalars().keys().cloned().collect();
    for key in scalar_keys {
        if let Some(value) = template.variable_store.scalars().get(&key) {
            let resolved = context::resolve_with_context(value.as_str(), &context);
            context.insert(key.clone(), resolved);
        }
    }

    let map_iteration = if iteration_pairs.is_empty() {
        None
    } else {
        let label = iteration_pairs
            .iter()
            .map(|(name, value)| format!("{}={}", name, value))
            .collect::<Vec<String>>()
            .join(", ");

        Some(MapIteration {
            variables: iteration_pairs,
            label,
        })
    };

    Ok((context, map_iteration))
}

fn validate_iteration_value(
    template: &RequestTemplate,
    variable: &str,
    value: &str,
) -> Result<(), TemplateError> {
    if value.trim().is_empty() {
        return Err(TemplateError::InvalidArrayValue {
            request: template.group_key.clone(),
            variable: variable.to_string(),
            value: value.to_string(),
        });
    }

    if value
        .chars()
        .any(|ch| ch.is_whitespace() || matches!(ch, '[' | ']'))
    {
        return Err(TemplateError::InvalidArrayValue {
            request: template.group_key.clone(),
            variable: variable.to_string(),
            value: value.to_string(),
        });
    }

    Ok(())
}

fn detect_array_variables(template: &RequestTemplate) -> Vec<String> {
    let mut found: HashSet<String> = HashSet::new();

    collect_from_string(&mut found, &template.variable_store, template.url.as_str());
    collect_from_string(
        &mut found,
        &template.variable_store,
        template.description_template.as_str(),
    );

    for value in template.headers.values() {
        collect_from_string(&mut found, &template.variable_store, value.as_str());
    }

    for value in template.query_params.values() {
        collect_from_string(&mut found, &template.variable_store, value.as_str());
    }

    for value in template.form_text.values() {
        collect_from_string(&mut found, &template.variable_store, value.as_str());
    }

    for value in template.form_files.values() {
        collect_from_string(&mut found, &template.variable_store, value.as_str());
    }

    if let Some(body) = &template.body {
        collect_from_string(&mut found, &template.variable_store, body.as_str());
    }

    if let Some(content_type) = &template.body_content_type {
        collect_from_string(&mut found, &template.variable_store, content_type.as_str());
    }

    for src in &template.callback_src {
        collect_from_string(&mut found, &template.variable_store, src.as_str());
    }

    for capture in &template.response_captures {
        collect_from_string(
            &mut found,
            &template.variable_store,
            capture.raw_path.as_str(),
        );
        if let Some(default) = &capture.default {
            collect_from_string(&mut found, &template.variable_store, default.as_str());
        }
    }

    for assertion in &template.assertions {
        collect_from_string(&mut found, &template.variable_store, assertion.raw.as_str());
    }

    let mut variables: Vec<String> = found.into_iter().collect();
    variables.sort();
    variables
}

fn collect_from_string(acc: &mut HashSet<String>, store: &VariableStore, value: &str) {
    for placeholder in context::extract_placeholders(value) {
        if store.contains_array(&placeholder) {
            acc.insert(placeholder);
        }
    }
}

fn resolve_dependencies(requests: &mut [Request]) -> Result<(), TemplateError> {
    let mut lookup: HashMap<String, Vec<String>> = HashMap::new();

    for request in requests.iter() {
        lookup
            .entry(request.base_description.clone())
            .or_default()
            .push(request.description.clone());
    }

    for request in requests.iter_mut() {
        let mut resolved = Vec::with_capacity(request.declared_dependencies.len());

        for dependency in &request.declared_dependencies {
            match lookup.get(dependency) {
                Some(actual) if actual.len() == 1 => resolved.push(actual[0].clone()),
                Some(_) => {
                    return Err(TemplateError::DependencyOnMappedRequest {
                        request: request.base_description.clone(),
                        dependency: dependency.clone(),
                    });
                }
                None => {
                    return Err(TemplateError::UnknownDependency {
                        request: request.base_description.clone(),
                        dependency: dependency.clone(),
                    });
                }
            }
        }

        request.dependencies = resolved;
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use http::Method;
    use tempfile::tempdir;

    fn make_store() -> VariableStore {
        let mut store = VariableStore::new();
        store.set_scalar("API".to_string(), "https://example.com".to_string());
        store
    }

    fn sorted_descriptions(requests: &[Request]) -> Vec<String> {
        let mut names: Vec<String> = requests.iter().map(|req| req.description.clone()).collect();
        names.sort();
        names
    }

    #[test]
    fn expands_map_requests_for_array_variables() {
        let mut store = make_store();
        store.set_array(
            "USER".to_string(),
            vec!["foo".to_string(), "bar".to_string()],
        );

        let template = RequestTemplate {
            group_key: "Fetch user".to_string(),
            description_template: "Fetch user".to_string(),
            method: Method::GET,
            url: "{{ API }}/users/{{ USER }}".to_string(),
            headers: HashMap::new(),
            query_params: HashMap::new(),
            form_text: HashMap::new(),
            form_files: HashMap::new(),
            body: None,
            body_content_type: None,
            callback_src: vec![],
            response_captures: vec![],
            assertions: vec![],
            fragment_includes: vec![],
            declared_dependencies: vec![],
            variable_store: store,
            working_dir: PathBuf::new(),
        };

        let requests = expand_templates(vec![template]).expect("expansion should succeed");
        assert_eq!(requests.len(), 2);

        let names = sorted_descriptions(&requests);
        assert!(names.iter().any(|name| name.contains("USER=foo")));
        assert!(names.iter().any(|name| name.contains("USER=bar")));

        for request in &requests {
            assert!(request.url.ends_with("/users/foo") || request.url.ends_with("/users/bar"));
            let iter = request
                .map_iteration
                .as_ref()
                .expect("map iteration should exist");
            assert_eq!(iter.variables.len(), 1);
            assert_eq!(iter.variables[0].0, "USER");
            assert!(iter.variables[0].1 == "foo" || iter.variables[0].1 == "bar");
            assert!(request.context.get("USER").is_some());
        }
    }

    #[test]
    fn dependency_on_mapped_request_is_rejected() {
        let mut store = make_store();
        store.set_array("ITEM".to_string(), vec!["a".to_string(), "b".to_string()]);

        let map_template = RequestTemplate {
            group_key: "Map Request".to_string(),
            description_template: "Map Request".to_string(),
            method: Method::GET,
            url: "/items/{{ ITEM }}".to_string(),
            headers: HashMap::new(),
            query_params: HashMap::new(),
            form_text: HashMap::new(),
            form_files: HashMap::new(),
            body: None,
            body_content_type: None,
            callback_src: vec![],
            response_captures: vec![],
            assertions: vec![],
            fragment_includes: vec![],
            declared_dependencies: vec![],
            variable_store: store,
            working_dir: PathBuf::new(),
        };

        let dependent_template = RequestTemplate {
            group_key: "Dependent".to_string(),
            description_template: "Dependent".to_string(),
            method: Method::GET,
            url: "/dependent".to_string(),
            headers: HashMap::new(),
            query_params: HashMap::new(),
            form_text: HashMap::new(),
            form_files: HashMap::new(),
            body: None,
            body_content_type: None,
            callback_src: vec![],
            response_captures: vec![],
            assertions: vec![],
            fragment_includes: vec![],
            declared_dependencies: vec!["Map Request".to_string()],
            variable_store: VariableStore::new(),
            working_dir: PathBuf::new(),
        };

        let err = expand_templates(vec![map_template, dependent_template])
            .expect_err("dependency on mapped request should fail");
        match err {
            TemplateError::DependencyOnMappedRequest { dependency, .. } => {
                assert_eq!(dependency, "Map Request");
            }
            other => panic!("unexpected error: {other:?}"),
        }
    }

    #[test]
    fn guarded_fragment_includes_when_predicate_true() {
        let dir = tempdir().unwrap();
        let fragment_path = dir.path().join("assertions.hen");
        std::fs::write(&fragment_path, "^ body.username == \"foo\"\n").unwrap();

        let mut store = make_store();
        store.set_scalar("USERNAME".to_string(), "foo".to_string());

        let base_assertion = Assertion::parse("^ status == 200", &store.clone_scalars()).unwrap();

        let template = RequestTemplate {
            group_key: "Guarded".to_string(),
            description_template: "Guarded".to_string(),
            method: Method::GET,
            url: "{{ API }}/users/{{ USERNAME }}".to_string(),
            headers: HashMap::new(),
            query_params: HashMap::new(),
            form_text: HashMap::new(),
            form_files: HashMap::new(),
            body: None,
            body_content_type: None,
            callback_src: vec![],
            response_captures: vec![],
            assertions: vec![base_assertion],
            fragment_includes: vec![FragmentInclude {
                path: fragment_path
                    .file_name()
                    .unwrap()
                    .to_string_lossy()
                    .to_string(),
                guard: Some("USERNAME == \"foo\"".to_string()),
                assertion_index: 1,
            }],
            declared_dependencies: vec![],
            variable_store: store,
            working_dir: dir.path().to_path_buf(),
        };

        let requests = expand_templates(vec![template]).expect("expansion should succeed");
        let request = requests.first().expect("request should exist");

        assert_eq!(request.assertions.len(), 2);
        assert!(request.assertions[1]
            .raw
            .contains("body.username == \"foo\""));
    }

    #[test]
    fn guarded_fragment_skips_when_predicate_false() {
        let dir = tempdir().unwrap();
        let fragment_path = dir.path().join("assertions.hen");
        std::fs::write(&fragment_path, "^ body.username == \"foo\"\n").unwrap();

        let mut store = make_store();
        store.set_scalar("USERNAME".to_string(), "bar".to_string());

        let base_assertion = Assertion::parse("^ status == 200", &store.clone_scalars()).unwrap();

        let template = RequestTemplate {
            group_key: "Guarded".to_string(),
            description_template: "Guarded".to_string(),
            method: Method::GET,
            url: "{{ API }}/users/{{ USERNAME }}".to_string(),
            headers: HashMap::new(),
            query_params: HashMap::new(),
            form_text: HashMap::new(),
            form_files: HashMap::new(),
            body: None,
            body_content_type: None,
            callback_src: vec![],
            response_captures: vec![],
            assertions: vec![base_assertion],
            fragment_includes: vec![FragmentInclude {
                path: fragment_path
                    .file_name()
                    .unwrap()
                    .to_string_lossy()
                    .to_string(),
                guard: Some("USERNAME == \"foo\"".to_string()),
                assertion_index: 1,
            }],
            declared_dependencies: vec![],
            variable_store: store,
            working_dir: dir.path().to_path_buf(),
        };

        let requests = expand_templates(vec![template]).expect("expansion should succeed");
        let request = requests.first().expect("request should exist");

        assert_eq!(request.assertions.len(), 1);
        assert!(request.assertions[0].raw.contains("status == 200"));
    }
}
