#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LineEntry {
    pub number: usize,
    pub raw_string: String,

    /// Used in ExtraBlankLineFixer
    pub is_deleted: bool,
    /// Used in EndingBlankLineChecker
    pub is_last_line: bool,
}

impl LineEntry {
    pub fn new<T>(number: usize, raw_string: T, is_last_line: bool) -> Self
    where
        T: Into<String>,
    {
        LineEntry {
            number,
            raw_string: raw_string.into(),
            is_deleted: false,
            is_last_line,
        }
    }

    pub fn is_empty_or_comment(&self) -> bool {
        self.is_empty() || self.is_comment()
    }

    pub fn is_empty(&self) -> bool {
        self.trimmed_string().is_empty()
    }

    pub fn is_comment(&self) -> bool {
        self.trimmed_string().starts_with('#')
    }

    pub fn get_key(&self) -> Option<&str> {
        if self.is_empty_or_comment() {
            return None;
        }

        let stripped = self.stripped_export_string();
        Some(stripped.split('=').next().unwrap_or(stripped))
    }

    pub fn get_value(&self) -> Option<&str> {
        if self.is_empty_or_comment() {
            return None;
        }

        self.raw_string
            .find('=')
            .map(|idx| &self.raw_string[(idx + 1)..])
    }

    fn trimmed_string(&self) -> &str {
        self.raw_string.trim()
    }

    fn stripped_export_string(&self) -> &str {
        let trimmed = self.trimmed_string();
        trimmed
            .strip_prefix("export ")
            .map(str::trim)
            .unwrap_or(trimmed)
    }

    pub fn mark_as_deleted(&mut self) {
        self.is_deleted = true;
    }

    // Maybe we should add the comment field to the LineEntry struct (but this requires some
    // refactoring of the line entries creation)
    // pub control_comment: Option<Comment<'a>>
    pub fn get_comment(&self) -> Option<&str> {
        if !self.is_comment() {
            return None;
        }

        Some(self.raw_string.as_str())
    }

    pub fn get_substitution_keys(&self) -> Vec<&str> {
        let mut keys = Vec::new();

        let mut value = match self.get_value().map(str::trim) {
            Some(value) if !value.starts_with('\'') => value,
            _ => return keys,
        };

        if value.starts_with('\"') {
            if value.len() > 1 && value.ends_with('\"') && !is_escaped(&value[..value.len() - 1]) {
                value = &value[1..value.len() - 1]
            } else {
                return keys;
            }
        }

        while let Some(index) = value.find('$') {
            let prefix = &value[..index];
            let raw_key = &value[index + 1..];

            if is_escaped(prefix) {
                value = raw_key;
            } else {
                let (key, rest) = raw_key
                    .strip_prefix('{')
                    .and_then(|raw_key| raw_key.find('}').map(|i| raw_key.split_at(i)))
                    .or_else(|| {
                        raw_key
                            .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
                            .map(|i| raw_key.split_at(i))
                    })
                    .unwrap_or((raw_key, ""));
                if key.is_empty() {
                    return keys;
                }

                keys.push(key);
                value = rest;
            }
        }
        keys
    }
}

pub fn is_escaped(prefix: &str) -> bool {
    prefix.chars().rev().take_while(|ch| *ch == '\\').count() % 2 == 1
}

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

    #[test]
    fn is_escaped_value_test() {
        let escaped = "\\";
        assert!(is_escaped(escaped));

        let escaped = "\\\\\\";
        assert!(is_escaped(escaped));

        let non_escaped = "\\\\";
        assert!(!is_escaped(non_escaped));

        let random_string = "text without escaping";
        assert!(!is_escaped(random_string));
    }

    pub fn line_entry(number: usize, total_lines: usize, raw_string: &str) -> LineEntry {
        LineEntry::new(number, raw_string, total_lines == number)
    }

    mod is_empty_or_comment {
        use super::*;

        #[test]
        fn run_with_empty_line_test() {
            let input = line_entry(1, 1, "");

            assert!(input.is_empty());
            assert!(!input.is_comment());
            assert!(input.is_empty_or_comment());
        }

        #[test]
        fn run_with_comment_line_test() {
            let input = line_entry(1, 1, "# Comment");

            assert!(!input.is_empty());
            assert!(input.is_comment());
            assert!(input.is_empty_or_comment());
        }

        #[test]
        fn run_with_not_comment_or_empty_line_test() {
            let input = line_entry(1, 1, "NotComment");

            assert!(!input.is_empty());
            assert!(!input.is_comment());
            assert!(!input.is_empty_or_comment());
        }
    }

    mod get_key {
        use super::*;

        #[test]
        fn empty_line_test() {
            let input = line_entry(1, 1, "");
            let expected = None;

            assert_eq!(expected, input.get_key());
        }

        #[test]
        fn stripped_export_prefix_test() {
            let input = line_entry(1, 1, "export FOO=BAR");
            let expected = Some("FOO");

            assert_eq!(expected, input.get_key());
        }

        #[test]
        fn correct_line_test() {
            let input = line_entry(1, 1, "FOO=BAR");
            let expected = Some("FOO");

            assert_eq!(expected, input.get_key());
        }

        #[test]
        fn line_without_value_test() {
            let input = line_entry(1, 1, "FOO=");
            let expected = Some("FOO");

            assert_eq!(expected, input.get_key());
        }

        #[test]
        fn missing_value_and_equal_sign_test() {
            let input = line_entry(1, 1, "FOOBAR");
            let expected = Some("FOOBAR");

            assert_eq!(expected, input.get_key());
        }
    }

    mod get_value {
        use super::*;

        #[test]
        fn empty_line_test() {
            let input = line_entry(1, 1, "");
            let expected = None;

            assert_eq!(expected, input.get_value());
        }

        #[test]
        fn correct_line_test() {
            let input = line_entry(1, 1, "FOO=BAR");
            let expected = Some("BAR");

            assert_eq!(expected, input.get_value());
        }

        #[test]
        fn correct_line_with_single_quote_test() {
            let input = line_entry(1, 1, "FOO='BAR'");
            let expected = Some("'BAR'");

            assert_eq!(expected, input.get_value());
        }

        #[test]
        fn correct_line_with_double_quote_test() {
            let input = line_entry(1, 1, "FOO=\"BAR\"");
            let expected = Some("\"BAR\"");

            assert_eq!(expected, input.get_value());
        }

        #[test]
        fn line_without_key_test() {
            let input = line_entry(1, 1, "=BAR");
            let expected = Some("BAR");

            assert_eq!(expected, input.get_value());
        }

        #[test]
        fn line_without_value_test() {
            let input = line_entry(1, 1, "FOO=");
            let expected = Some("");

            assert_eq!(expected, input.get_value());
        }

        #[test]
        fn missing_value_and_equal_sign_test() {
            let input = line_entry(1, 1, "FOOBAR");
            let expected = None;

            assert_eq!(expected, input.get_value());
        }
    }

    mod trimmed_string {
        use super::*;

        #[test]
        fn line_without_blank_chars_test() {
            let entry = line_entry(1, 1, "FOO=BAR");

            assert_eq!("FOO=BAR", entry.trimmed_string());
        }

        #[test]
        fn line_with_spaces_test() {
            let entry = line_entry(1, 1, "   FOO=BAR  ");

            assert_eq!("FOO=BAR", entry.trimmed_string());
        }

        #[test]
        fn line_with_tab_test() {
            let entry = line_entry(1, 1, "FOO=BAR\t");

            assert_eq!("FOO=BAR", entry.trimmed_string());
        }
    }

    mod get_comment {
        use super::*;

        #[test]
        fn line_with_comment_test() {
            let entry = line_entry(1, 1, "# dotenv-linter:off LowercaseKey");
            let comment = entry.get_comment();
            assert!(comment.is_some());

            // TODO: Update later
            // let comment = entry.get_comment().expect("comment");
            // assert_eq!(comment.is_disabled(), true);
            // assert_eq!(comment.checks, vec![LintKind::LowercaseKey]);
        }

        #[test]
        fn line_with_no_comment_test() {
            let entry = line_entry(1, 1, "A=B");
            let comment = entry.get_comment();
            assert!(comment.is_none());
        }
    }

    mod get_substitution_keys {
        use super::*;

        #[test]
        fn run_with_empty() {
            let input = line_entry(1, 1, "");
            assert!(input.get_substitution_keys().is_empty());
        }

        #[test]
        fn run_with_simple() {
            let input = line_entry(1, 1, "FOO=$BAR");
            assert_eq!(input.get_substitution_keys(), vec!["BAR"]);
        }

        #[test]
        fn run_with_simple_comment() {
            let input = line_entry(1, 1, "FOO=$BAR # comment");
            assert_eq!(input.get_substitution_keys(), vec!["BAR"]);
        }

        #[test]
        fn run_with_curly_braces() {
            let input = line_entry(1, 1, "FOO=${BAR}");
            assert_eq!(input.get_substitution_keys(), vec!["BAR"]);

            let input = line_entry(1, 1, "FOO=$BAR}");
            assert_eq!(input.get_substitution_keys(), vec!["BAR"]);

            let input = line_entry(1, 1, "FOO=${BAR");
            assert!(input.get_substitution_keys().is_empty());
        }

        #[test]
        fn run_with_double_quotes() {
            let input = line_entry(1, 1, r#"FOO="$BAR""#);
            assert_eq!(input.get_substitution_keys(), vec!["BAR"]);

            let input = line_entry(1, 1, r#"FOO=$BAR""#);
            assert_eq!(input.get_substitution_keys(), vec!["BAR"]);

            let input = line_entry(1, 1, r#"FOO="$BAR"#);
            assert!(input.get_substitution_keys().is_empty());

            let input = line_entry(1, 1, r#"FOO="$BAR\""#);
            assert!(input.get_substitution_keys().is_empty());

            let input = line_entry(1, 1, r#"FOO="\""#);
            assert!(input.get_substitution_keys().is_empty());

            let input = line_entry(1, 1, r#"FOO="${BAR}\\""#);
            assert_eq!(input.get_substitution_keys(), vec!["BAR"]);
        }

        #[test]
        fn run_with_single_quotes() {
            let input = line_entry(1, 1, "FOO='$BAR'");
            assert!(input.get_substitution_keys().is_empty());

            let input = line_entry(1, 1, r"FOO=TEST_${BAR}_\'");
            assert_eq!(input.get_substitution_keys(), vec!["BAR"]);
        }

        #[test]
        fn run_with_escaped_dollar() {
            let input = line_entry(1, 1, r"FOO=\$BAR");
            assert!(input.get_substitution_keys().is_empty());

            let input = line_entry(1, 1, r"FOO=\\$BAR");
            assert_eq!(input.get_substitution_keys(), vec!["BAR"]);

            let input = line_entry(1, 1, r"FOO=\\\$BAR");
            assert!(input.get_substitution_keys().is_empty());
        }

        #[test]
        fn run_with_complicated() {
            let input = line_entry(1, 1, "DATABASE=postgres://${USER}@localhost/database");
            assert_eq!(input.get_substitution_keys(), vec!["USER"]);
        }

        #[test]
        fn run_with_reused() {
            let input = line_entry(1, 1, "FOO=$BAR$BAR");
            assert_eq!(input.get_substitution_keys(), vec!["BAR", "BAR"]);

            let input = line_entry(1, 1, "FOO=${BAR}${BAR}");
            assert_eq!(input.get_substitution_keys(), vec!["BAR", "BAR"]);

            let input = line_entry(1, 1, "FOO=${BAR}${BAZ}");
            assert_eq!(input.get_substitution_keys(), vec!["BAR", "BAZ"]);
        }

        #[test]
        fn run_with_break() {
            let input = line_entry(1, 1, "FOO=${BAR $BAZ");
            assert!(input.get_substitution_keys().is_empty());
        }
    }
}
