use anyhow::Result;
use path2regex::{
    CompilerBuilder, CompilerOptions, Key, MatchResult, MatcherBuilder, MatcherOptions, Parser,
    ParserOptions, PathRegex, PathRegexBuilder, PathRegexOptions, Token, TryIntoWith,
};
use serde_json::{json, Value};

struct CompileCase<'a> {
    params: Value,
    result: &'a str,
    options: CompilerOptions,
}

impl<'a> Default for CompileCase<'a> {
    fn default() -> Self {
        Self {
            params: json!({}),
            result: "",
            options: Default::default(),
        }
    }
}

#[derive(Default)]
struct MatchCase<'a> {
    path_name: &'a str,
    matches: Option<Vec<&'a str>>,
    params: Option<MatchResult>,
    options: MatcherOptions,
}

fn assert_re(
    path: impl TryIntoWith<PathRegex, PathRegexOptions>,
    tokens: &[Token],
    options: PathRegexOptions,
    should_parse_keys: bool,
) -> Result<PathRegex> {
    let re = PathRegexBuilder::new_with_options(path, options).build()?;
    let keys = re.keys();
    if should_parse_keys {
        let keys_in_tokens = tokens
            .iter()
            .map(|token| match token {
                Token::Key(key) => key.clone(),
                _ => Key::default(),
            })
            .filter(|x| !x.name.is_empty())
            .collect::<Vec<_>>();
        assert_eq!(keys, &keys_in_tokens, "should parse keys");
    }
    Ok(re)
}

fn assert_parse(path: impl AsRef<str>, tokens: &Vec<Token>, options: ParserOptions) -> Result<()> {
    let parser = Parser::new_with_options(options);
    assert_eq!(&parser.parse_str(path)?, tokens, "should parse");
    Ok(())
}

fn assert_compile(
    path: impl TryIntoWith<Vec<Token>, ParserOptions>,
    complie_cases: &Vec<CompileCase>,
    options: CompilerOptions,
) -> Result<()> {
    for case in complie_cases {
        #[allow(clippy::needless_update)]
        let options = CompilerOptions {
            delimiter: options.delimiter.clone(),
            prefixes: options.prefixes.clone(),
            sensitive: options.sensitive,
            encode: options.encode,
            validate: options.validate,
            ..case.options
        };
        let compiler = CompilerBuilder::new_with_options(path.clone(), options).build()?;
        if case.result.is_empty() {
            assert!(
                compiler.render(&case.params).is_err(),
                "should not compile using {}",
                case.params
            );
        } else {
            assert_eq!(
                compiler.render(&case.params)?,
                case.result,
                "should compile using {}",
                case.params
            );
        }
    }
    Ok(())
}

fn assert_match(
    path: impl TryIntoWith<PathRegex, PathRegexOptions>,
    re: &PathRegex,
    match_cases: &Vec<MatchCase>,
) -> Result<()> {
    for case in match_cases {
        let message = format!(
            "should {}match {}",
            if case.matches.is_none() { "not " } else { "" },
            case.path_name
        );
        let matches = re.captures(case.path_name).map(|cap| {
            cap.iter()
                .map(|x| match x {
                    Some(x) => x.as_str(),
                    None => Default::default(),
                })
                .collect::<Vec<_>>()
        });

        assert_eq!(matches, case.matches, "{}", message);

        if case.params.is_some() {
            let matcher =
                MatcherBuilder::new_with_options(path.clone(), case.options.clone()).build()?;
            assert_eq!(
                matcher.find(case.path_name),
                case.params,
                "{} params",
                message
            );
        }
    }
    Ok(())
}

#[test]
fn test_rule_1() -> Result<()> {
    let path = "/";
    let ops = PathRegexOptions::default();
    let tokens = vec![Token::Static("/".to_owned())];

    let re = assert_re(path, &tokens, ops.clone(), false)?;

    assert_parse(path, &tokens, ParserOptions::from(ops))?;

    assert_compile(
        path,
        &vec![
            CompileCase {
                result: "/",
                ..Default::default()
            },
            CompileCase {
                params: json!({"id":123}),
                result: "/",
                ..Default::default()
            },
        ],
        CompilerOptions::default(),
    )?;

    assert_match(
        path,
        &re,
        &vec![
            MatchCase {
                path_name: "/",
                matches: Some(vec!["/"]),
                params: Some(MatchResult {
                    path: "/".to_owned(),
                    index: 0,
                    params: json!({}),
                }),
                ..Default::default()
            },
            MatchCase {
                path_name: "/route",
                ..Default::default()
            },
        ],
    )?;

    Ok(())
}

#[test]
fn test_rule_2() -> Result<()> {
    let path = "/test";
    let ops = PathRegexOptions::default();
    let tokens = vec![Token::Static("/test".to_owned())];

    let re = assert_re(path, &tokens, ops.clone(), false)?;

    assert_parse(path, &tokens, ParserOptions::from(ops))?;

    assert_compile(
        path,
        &vec![CompileCase {
            result: "/test",
            ..Default::default()
        }],
        CompilerOptions::default(),
    )?;

    assert_match(
        path,
        &re,
        &vec![
            MatchCase {
                path_name: "/test",
                matches: Some(vec!["/test"]),
                params: Some(MatchResult {
                    path: "/test".to_owned(),
                    index: 0,
                    params: json!({}),
                }),
                ..Default::default()
            },
            MatchCase {
                path_name: "/route",
                ..Default::default()
            },
            MatchCase {
                path_name: "/test/route",
                ..Default::default()
            },
            MatchCase {
                path_name: "/test/",
                matches: Some(vec!["/test/"]),
                params: Some(MatchResult {
                    path: "/test/".to_owned(),
                    index: 0,
                    params: json!({}),
                }),
                ..Default::default()
            },
        ],
    )?;

    Ok(())
}

#[test]
fn test_rule_3() -> Result<()> {
    let path = "/test/";
    let ops = PathRegexOptions::default();
    let tokens = vec![Token::Static("/test/".to_owned())];

    let re = assert_re(path, &tokens, ops.clone(), false)?;

    assert_parse(path, &tokens, ParserOptions::from(ops))?;

    assert_compile(
        path,
        &vec![CompileCase {
            result: "/test/",
            ..Default::default()
        }],
        CompilerOptions::default(),
    )?;

    assert_match(
        path,
        &re,
        &vec![
            MatchCase {
                path_name: "/test",
                matches: None,
                ..Default::default()
            },
            MatchCase {
                path_name: "/test/",
                matches: Some(vec!["/test/"]),
                ..Default::default()
            },
            MatchCase {
                path_name: "/test//",
                matches: Some(vec!["/test//"]),
                ..Default::default()
            },
        ],
    )?;

    Ok(())
}

#[test]
fn test_rule_4() -> Result<()> {
    let path = "/test";
    let ops = PathRegexOptions {
        sensitive: true,
        ..PathRegexOptions::default()
    };
    let tokens = vec![Token::Static("/test".to_owned())];

    let re = assert_re(path, &tokens, ops.clone(), false)?;

    assert_parse(path, &tokens, ParserOptions::from(ops))?;

    assert_match(
        path,
        &re,
        &vec![
            MatchCase {
                path_name: "/test",
                matches: Some(vec!["/test"]),
                ..Default::default()
            },
            MatchCase {
                path_name: "/TEST",
                ..Default::default()
            },
        ],
    )?;

    assert_compile(
        path,
        &vec![CompileCase {
            result: "/test",
            ..Default::default()
        }],
        CompilerOptions::default(),
    )?;

    Ok(())
}

#[test]
fn test_rule_5() -> Result<()> {
    let path = "/test";
    let ops = PathRegexOptions {
        strict: true,
        ..PathRegexOptions::default()
    };
    let tokens = vec![Token::Static("/test".to_owned())];

    let re = assert_re(path, &tokens, ops.clone(), false)?;

    assert_parse(path, &tokens, ParserOptions::from(ops))?;

    assert_match(
        path,
        &re,
        &vec![
            MatchCase {
                path_name: "/test",
                matches: Some(vec!["/test"]),
                ..Default::default()
            },
            MatchCase {
                path_name: "/test/",
                ..Default::default()
            },
            MatchCase {
                path_name: "/TEST",
                matches: Some(vec!["/TEST"]),
                ..Default::default()
            },
        ],
    )?;

    assert_compile(
        path,
        &vec![CompileCase {
            result: "/test",
            ..Default::default()
        }],
        CompilerOptions::default(),
    )?;

    Ok(())
}

#[test]
fn test_rule_6() -> Result<()> {
    let path = "/test/";
    let ops = PathRegexOptions {
        strict: true,
        ..PathRegexOptions::default()
    };
    let tokens = vec![Token::Static("/test/".to_owned())];

    let re = assert_re(path, &tokens, ops.clone(), false)?;

    assert_parse(path, &tokens, ParserOptions::from(ops))?;

    assert_match(
        path,
        &re,
        &vec![
            MatchCase {
                path_name: "/test",
                ..Default::default()
            },
            MatchCase {
                path_name: "/test/",
                matches: Some(vec!["/test/"]),
                ..Default::default()
            },
            MatchCase {
                path_name: "/test//",
                ..Default::default()
            },
        ],
    )?;

    assert_compile(
        path,
        &vec![CompileCase {
            result: "/test/",
            ..Default::default()
        }],
        CompilerOptions::default(),
    )?;

    Ok(())
}

#[test]
fn test_rule_7() -> Result<()> {
    let path = "/test";
    let ops = PathRegexOptions {
        end: false,
        ..PathRegexOptions::default()
    };
    let tokens = vec![Token::Static("/test".to_owned())];

    let re = assert_re(path, &tokens, ops.clone(), false)?;

    assert_parse(path, &tokens, ParserOptions::from(ops))?;

    assert_match(
        path,
        &re,
        &vec![
            MatchCase {
                path_name: "/test",
                matches: Some(vec!["/test", ""]),
                ..Default::default()
            },
            MatchCase {
                path_name: "/test/",
                matches: Some(vec!["/test/", ""]),
                ..Default::default()
            },
            MatchCase {
                path_name: "/test/route",
                matches: Some(vec!["/test/", "/"]),
                ..Default::default()
            },
            MatchCase {
                path_name: "/route",
                ..Default::default()
            },
        ],
    )?;

    assert_compile(
        path,
        &vec![CompileCase {
            result: "/test",
            ..Default::default()
        }],
        CompilerOptions::default(),
    )?;

    Ok(())
}