use std::collections::HashMap;
use std::fs::File;
use std::hash::Hash;
use std::io::{BufRead, BufReader, Lines, Read};
use std::iter::Iterator;
use std::path::Path;

use phf::phf_map;

use crate::entry::POEntry;
use crate::errors::{MaybeFilename, SyntaxError};
use crate::escaping::unescape;
use crate::file::{pofile::POFile, FileOptions};

#[derive(Hash, Eq, PartialEq, Clone, Copy, Debug)]

pub enum St {
    ST, // Beginning of the file (start)
    HE, // Header
    TC, // a translation comment
    GC, // a generated comment
    OC, // a file/line occurrence
    FL, // a flags line
    CT, // a message context
    PC, // a previous msgctxt
    PM, // a previous msgid
    PP, // a previous msgid_plural
    MI, // a msgid
    MP, // a msgid plural
    MS, // a msgstr
    MX, // a msgstr plural
    MC, // a msgid or msgstr continuation line
}

static KEYWORDS: phf::Map<&'static str, &'static St> = phf_map! {
    "msgctxt" => &St::CT,
    "msgid" => &St::MI,
    "msgstr" => &St::MS,
    "msgid_plural" => &St::MP,
};

static PREV_KEYWORDS: phf::Map<&'static str, &'static St> = phf_map! {
    "msgid_plural" => &St::PP,
    "msgid" => &St::PM,
    "msgctxt" => &St::PC,
};

type TransitionFn = dyn Fn(&mut POFileParser);
type Symbol = St;
type CurrentSt = St;
type Action = St;
type NextSt = St;
pub type Transitions = HashMap<(Symbol, CurrentSt), (Action, NextSt)>;

// Transitions are generated by the build.rs script
include!(concat!(env!("OUT_DIR"), "/poparser-transitions.rs"));

struct LinesHandler<'a> {
    lines: Lines<BufReader<&'a mut dyn Read>>,
}

impl LinesHandler<'_> {
    fn new(handler: &mut dyn Read) -> LinesHandler {
        LinesHandler {
            lines: BufReader::new(handler).lines(),
        }
    }
}

impl Iterator for LinesHandler<'_> {
    type Item = String;

    fn next(&mut self) -> Option<Self::Item> {
        match self.lines.next() {
            Some(Ok(line)) => Some(line),
            Some(Err(_)) => None,
            None => None,
        }
    }
}

pub(crate) struct POFileParser {
    content_is_path: bool,

    pub file: POFile,

    current_state: St,
    current_token: String,
    current_line: usize,
    current_entry: POEntry,
    msgstr_index: usize,
    entry_obsolete: bool,
    transitions: Transitions,
}

impl POFileParser {
    pub fn new(file_options: FileOptions) -> POFileParser {
        POFileParser {
            content_is_path: Path::new(&file_options.path_or_content)
                .is_file(),

            file: POFile::new(file_options),

            current_state: St::ST,
            current_token: String::new(),
            current_line: 0,
            current_entry: POEntry::new(0),
            msgstr_index: 0,
            entry_obsolete: false,
            transitions: build_transitions(),
        }
    }

    fn maybe_add_current_entry(&mut self) {
        if [St::MC, St::MS, St::MX].contains(&self.current_state) {
            let new_entry = self.current_entry.clone();
            self.file.entries.push(new_entry);
            self.current_entry = POEntry::new(self.current_line);
            self.msgstr_index = 0;
        }
    }

    fn process(&mut self, symbol: &Symbol) {
        let next_transition = (*symbol, self.current_state);
        let (action, next_state) =
            *self.transitions.get(&next_transition).unwrap();

        (transition_fn_factory(action))(self);
        if action != St::MC {
            // if not in a message continuation line, change the state
            self.current_state = next_state;
        }
    }

    pub fn parse(&mut self) -> Result<(), SyntaxError> {
        if self.content_is_path {
            self.parse_file()?;
        } else {
            self.parse_content()?;
        }
        Ok(())
    }

    fn parse_file(&mut self) -> Result<(), SyntaxError> {
        let mut buf = BufReader::new(
            File::open(&self.file.options.path_or_content).unwrap(),
        );
        let mut handler = LinesHandler::new(&mut buf);
        self.parse_with_handler(&mut handler)?;
        Ok(())
    }

    fn parse_content(&mut self) -> Result<(), SyntaxError> {
        let content = self.file.options.path_or_content.clone();
        let mut buf = BufReader::new(content.as_bytes());
        let mut handler = LinesHandler::new(&mut buf);
        self.parse_with_handler(&mut handler)?;
        Ok(())
    }

    fn parse_with_handler(
        &mut self,
        handler: &mut LinesHandler,
    ) -> Result<(), SyntaxError> {
        let first_line = handler.next().unwrap_or("".to_string());
        self.parse_line(maybe_lstrip_utf8_bom(&first_line))?;

        for line in handler.by_ref() {
            self.parse_line(&line)?;
        }

        if self.current_entry.msgid.is_empty() {
            // Adding header entry
            if let Some(msgstr) = &self.current_entry.msgstr {
                if !msgstr.is_empty() {
                    self.file
                        .entries
                        .push(self.current_entry.clone());
                }
            }
        } else {
            self.file.entries.push(self.current_entry.clone());
        }

        let metadata_entry = self.file.find_by_msgid("");
        if let Some(metadata_entry) = metadata_entry {
            // Remove header from entries and store it in metadata hashmap
            self.file.metadata_is_fuzzy =
                !metadata_entry.flags.is_empty();
            self.file.remove(&metadata_entry);

            for metadata_line in
                metadata_entry.msgstr.unwrap().split("\\n")
            {
                let (key, value) =
                    match metadata_line.split_once(": ") {
                        Some((key, value)) => (key, value),
                        None => continue,
                    };
                if !self.file.metadata.contains_key(key) {
                    self.file.metadata.insert(
                        key.to_string(),
                        value.trim().to_string(),
                    );
                } else {
                    let mut new_value =
                        self.file.metadata.remove(key).unwrap();
                    new_value.push_str(value.trim());
                    self.file
                        .metadata
                        .insert(key.to_string(), new_value);
                }
            }
        }

        Ok(())
    }

    fn tokens_from_line(&self, line: &str) -> Vec<String> {
        let mut tokens: Vec<String> = Vec::with_capacity(3);
        for token in line.split_ascii_whitespace() {
            tokens.push(token.to_string());
            if tokens.len() == 3 {
                break;
            }
        }
        tokens
    }

    fn parse_line(&mut self, line: &str) -> Result<(), SyntaxError> {
        self.current_line += 1;

        let mut line = line.trim();
        if line.is_empty() {
            return Ok(());
        }

        let mut tokens = self.tokens_from_line(line);
        let mut nb_tokens = tokens.len();
        if nb_tokens == 0 || tokens[0] == "#~|" {
            return Ok(());
        } else if nb_tokens > 1 && tokens[0] == "#~" {
            line = line[3..].trim();
            tokens = tokens[1..].to_vec();
            nb_tokens -= 1;
            self.entry_obsolete = true
        } else {
            self.entry_obsolete = false;
        }

        if nb_tokens > 1 && KEYWORDS.contains_key(&tokens[0]) {
            line = line[tokens[0].len()..].trim_start();

            maybe_raise_unescaped_double_quote_found_error(
                line,
                self.current_line,
                self.content_is_path,
                &self.file.options.path_or_content,
                // +2 taking into account ' "' (space and first
                // double quote)
                tokens[0].chars().count() + 2,
            )?;
            self.current_token = line.to_string();
            let symbol = *KEYWORDS.get(&tokens[0]).unwrap();
            self.process(symbol);
            return Ok(());
        }

        self.current_token = line.to_string();
        if tokens[0] == "#:" {
            if nb_tokens <= 1 {
                return Ok(());
            }
            // occurrences
            self.process(&St::OC);
        } else if line.starts_with('"') {
            // continuation line
            maybe_raise_unescaped_double_quote_found_error(
                line,
                self.current_line,
                self.content_is_path,
                &self.file.options.path_or_content,
                1,
            )?;
            self.process(&St::MC);
        } else if self.current_token.starts_with("msgstr[") {
            // msgstr plural
            let index = self
                .current_token
                .splitn(2, '[')
                .last()
                .unwrap()
                .split(']')
                .next()
                .unwrap();
            match index.parse::<usize>() {
                Ok(index) => {
                    self.msgstr_index = index;
                }
                Err(_) => {
                    return Err(SyntaxError::Custom {
                        maybe_filename: MaybeFilename::new(
                            &self.file.options.path_or_content,
                            self.content_is_path,
                        ),
                        message: format!(
                            concat!(
                                "Invalid msgstr plural index.",
                                " Expected digit, found '{}'."
                            ),
                            index,
                        ),
                        line: self.current_line,
                        index: 7,
                    });
                }
            };
            self.process(&St::MX);
        } else if tokens[0] == "#," {
            if nb_tokens < 2 {
                return Ok(());
            }
            // flags line
            self.process(&St::FL);
        } else if tokens[0] == "#" {
            if line == "#" {
                //line.push(' ');
            }
            // translator comment
            self.process(&St::TC);
        } else if tokens[0].starts_with("##") {
            // translator comment
            self.process(&St::TC);
        } else if tokens[0] == "#." {
            if nb_tokens < 2 {
                return Ok(());
            }
            // generated comment
            self.process(&St::GC);
        } else if tokens[0] == "#|" {
            if nb_tokens < 2 {
                return Err(SyntaxError::Custom {
                    maybe_filename: MaybeFilename::new(
                        &self.file.options.path_or_content,
                        self.content_is_path,
                    ),
                    line: self.current_line,
                    index: 2,
                    message: "empty previous message found"
                        .to_string(),
                });
            }

            // Remove the marker and any whitespace following it
            if tokens[1].starts_with('"') {
                // Continuation of previous metadata
                self.process(&St::MC);
                return Ok(());
            }

            if nb_tokens == 2 {
                return Err(SyntaxError::Custom {
                    message: "invalid continuation line".to_string(),
                    maybe_filename: MaybeFilename::new(
                        &self.file.options.path_or_content,
                        self.content_is_path,
                    ),
                    line: self.current_line,
                    index: 0,
                });
            }

            // "previous translation" comment line
            if !PREV_KEYWORDS.contains_key(&tokens[1]) {
                // Unknown keyword in previous translation comment
                return Err(SyntaxError::Custom {
                    message: format!("unknown keyword {}", tokens[1]),
                    maybe_filename: MaybeFilename::new(
                        &self.file.options.path_or_content,
                        self.content_is_path,
                    ),
                    line: self.current_line,
                    index: 0,
                });
            }

            // Remove the keyword and any whitespace
            // between it and the starting quote
            self.current_token = line
                [tokens[1].len() + tokens[0].len() + 1..]
                .trim_start()
                .to_string();
            self.process(PREV_KEYWORDS.get(&tokens[1]).unwrap())
        } else {
            return Err(SyntaxError::Generic {
                maybe_filename: MaybeFilename::new(
                    &self.file.options.path_or_content,
                    self.content_is_path,
                ),
                line: self.current_line,
                index: 0,
            });
        }
        Ok(())
    }
}

fn handle_he(parser: &mut POFileParser) {
    let mut newheader = match parser.file.header {
        Some(ref header) => header,
        None => "",
    }
    .to_string();
    if !newheader.is_empty() {
        newheader.push('\n');
    }
    if parser.current_token.len() > 2 {
        newheader.push_str(&parser.current_token[2..]);
    }
    parser.file.header = Some(newheader);
}

fn handle_tc(parser: &mut POFileParser) {
    parser.maybe_add_current_entry();
    let optional_tcomment = parser.current_entry.tcomment.as_mut();
    let mut tcomment = match optional_tcomment {
        Some(tcomment) => {
            let t = tcomment;
            t.push('\n');
            t
        }
        None => "",
    }
    .to_string();

    let mut toappend =
        parser.current_token.trim_start_matches('#').to_string();
    if toappend.starts_with(' ') {
        toappend = toappend[1..].to_string();
    }
    tcomment.push_str(&toappend);

    parser.current_entry.tcomment =
        Some(tcomment.as_str().to_string());
}

fn handle_gc(parser: &mut POFileParser) {
    parser.maybe_add_current_entry();

    let optional_comment = parser.current_entry.comment.as_mut();
    let mut comment = match optional_comment {
        Some(comment) => {
            let t = comment;
            t.push('\n');
            t
        }
        None => "",
    }
    .to_string();

    if parser.current_token.len() > 3 {
        comment.push_str(&parser.current_token[3..]);
    }
    parser.current_entry.comment = Some(comment);
}

fn handle_oc(parser: &mut POFileParser) {
    parser.maybe_add_current_entry();
    for occ in parser.current_token[3..].split_whitespace() {
        if !occ.is_empty() {
            let (mut fil, mut line) =
                occ.split_once(':').unwrap_or((occ, ""));
            let mut line_isdigit = true;
            for c in line.chars() {
                if !c.is_ascii_digit() {
                    line_isdigit = false;
                    break;
                }
            }
            if !line_isdigit {
                fil = occ;
                line = "";
            }
            parser
                .current_entry
                .occurrences
                .push((fil.to_string(), line.to_string()))
        }
    }
}

fn handle_fl(parser: &mut POFileParser) {
    parser.maybe_add_current_entry();
    if parser.current_token.len() > 3 {
        let current_token_split =
            parser.current_token[3..].split(',');
        for substr in current_token_split {
            parser
                .current_entry
                .flags
                .push(substr.trim().to_string());
        }
    }
}

fn handle_pp(parser: &mut POFileParser) {
    parser.maybe_add_current_entry();
    parser.current_entry.previous_msgid_plural = Some(unescape(
        &parser.current_token[1..parser.current_token.len() - 1],
    ));
}

fn handle_pm(parser: &mut POFileParser) {
    parser.maybe_add_current_entry();
    parser.current_entry.previous_msgid = Some(unescape(
        &parser.current_token[1..parser.current_token.len() - 1],
    ));
}

fn handle_pc(parser: &mut POFileParser) {
    parser.maybe_add_current_entry();
    parser.current_entry.previous_msgctxt = Some(unescape(
        &parser.current_token[1..parser.current_token.len() - 1],
    ));
}

fn handle_ct(parser: &mut POFileParser) {
    parser.maybe_add_current_entry();
    parser.current_entry.msgctxt = Some(unescape(
        &parser.current_token[1..parser.current_token.len() - 1],
    ));
}

fn handle_mi(parser: &mut POFileParser) {
    parser.maybe_add_current_entry();
    parser.current_entry.obsolete = parser.entry_obsolete;
    parser.current_entry.msgid = unescape(
        &parser.current_token[1..parser.current_token.len() - 1],
    );
}

fn handle_mp(parser: &mut POFileParser) {
    parser.current_entry.msgid_plural = Some(unescape(
        &parser.current_token[1..parser.current_token.len() - 1],
    ));
}

fn handle_ms(parser: &mut POFileParser) {
    parser.current_entry.msgstr = Some(unescape(
        &parser.current_token[1..parser.current_token.len() - 1],
    ));
}

fn handle_mx(parser: &mut POFileParser) {
    let value = unescape(
        &parser.current_token[parser.current_token.find('"').unwrap()
            + 1
            ..parser.current_token.len() - 1],
    );
    let msgstr_plural_length =
        parser.current_entry.msgstr_plural.len();
    if parser.msgstr_index + 1 > msgstr_plural_length {
        for _ in 0..parser.msgstr_index + 1 - msgstr_plural_length {
            parser.current_entry.msgstr_plural.push("".to_string());
        }
    }

    parser.current_entry.msgstr_plural[parser.msgstr_index] = value;
}

fn handle_mc(parser: &mut POFileParser) {
    let token = unescape(
        &parser.current_token[1..&parser.current_token.len() - 1],
    );

    if parser.current_state == St::MI {
        parser.current_entry.msgid.push_str(&token);
    } else if parser.current_state == St::MS {
        let msgstr = parser.current_entry.msgstr.as_mut().unwrap();
        msgstr.push_str(&token);
        parser.current_entry.msgstr = Some(msgstr.to_string());
    } else if parser.current_state == St::CT {
        let msgctxt = parser.current_entry.msgctxt.as_mut().unwrap();
        msgctxt.push_str(&token);
        parser.current_entry.msgctxt = Some(msgctxt.to_string());
    } else if parser.current_state == St::MP {
        let msgid_plural =
            parser.current_entry.msgid_plural.as_mut().unwrap();
        msgid_plural.push_str(&token);
        parser.current_entry.msgid_plural =
            Some(msgid_plural.to_string());
    } else if parser.current_state == St::MX {
        parser.current_entry.msgstr_plural[parser.msgstr_index]
            .push_str(&token);
    } else if parser.current_state == St::PP {
        let previous_msgid_plural = parser
            .current_entry
            .previous_msgid_plural
            .as_mut()
            .unwrap();
        previous_msgid_plural.push_str(&token);
        parser.current_entry.previous_msgid_plural =
            Some(previous_msgid_plural.to_string());
    } else if parser.current_state == St::PM {
        let previous_msgid =
            parser.current_entry.previous_msgid.as_mut().unwrap();
        previous_msgid.push_str(&token);
        parser.current_entry.previous_msgid =
            Some(previous_msgid.to_string());
    } else if parser.current_state == St::PC {
        let previous_msgctxt =
            parser.current_entry.previous_msgctxt.as_mut().unwrap();
        previous_msgctxt.push_str(&token);
        parser.current_entry.previous_msgctxt =
            Some(previous_msgctxt.to_string());
    }
}

fn transition_fn_factory(action: Action) -> &'static TransitionFn {
    match action {
        St::HE => &handle_he,
        St::TC => &handle_tc,
        St::GC => &handle_gc,
        St::OC => &handle_oc,
        St::FL => &handle_fl,
        St::PP => &handle_pp,
        St::PM => &handle_pm,
        St::PC => &handle_pc,
        St::CT => &handle_ct,
        St::MI => &handle_mi,
        St::MP => &handle_mp,
        St::MS => &handle_ms,
        St::MX => &handle_mx,
        St::MC => &handle_mc,
        _ => panic!("Unknown action {:?}", action),
    }
}

fn maybe_lstrip_utf8_bom(line: &str) -> &str {
    line.trim_start_matches('\u{feff}')
}

fn find_unescaped_double_quote_index(line: &str) -> Option<usize> {
    let mut escaped = false;
    for (i, c) in line.chars().enumerate() {
        if c == '"' && !escaped {
            return Some(i);
        } else if c == '\\' {
            escaped = !escaped;
        } else {
            escaped = false;
        }
    }
    None
}

fn maybe_raise_unescaped_double_quote_found_error(
    text: &str,
    linenum: usize,
    path_or_content_is_path: bool,
    path_or_content: &str,
    index_offset: usize,
) -> Result<(), SyntaxError> {
    let text_str = text.to_string();
    let unescaped_double_quote_i = find_unescaped_double_quote_index(
        &text_str[1..text_str.len() - 1],
    );
    if let Some(double_quote_i) = unescaped_double_quote_i {
        return Err(SyntaxError::UnescapedDoubleQuoteFound {
            maybe_filename: MaybeFilename::new(
                path_or_content,
                path_or_content_is_path,
            ),
            line: linenum,
            index: double_quote_i + 1 + index_offset,
        });
    }
    Ok(())
}

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

    #[test]
    fn pofile_parser_constructor() {
        let path = "tests-data/empty.po";
        let content: String = fs::read_to_string(path).unwrap();

        // init from file path
        let parser = POFileParser::new(path.into());

        assert_eq!(parser.file.options.path_or_content, path);
        assert_eq!(parser.content_is_path, true);
        assert_eq!(parser.file.options.wrapwidth, 78);

        assert_eq!(parser.current_line, 0);
        assert_eq!(parser.current_entry.msgid, "");
        assert_eq!(parser.current_entry.linenum, 0);
        assert_eq!(parser.msgstr_index, 0);

        // init from file path and wrapwidth
        let parser = POFileParser::new((path, 30).into());
        assert_eq!(parser.file.options.path_or_content, path);
        assert_eq!(parser.content_is_path, true);
        assert_eq!(parser.file.options.wrapwidth, 30);

        // init from file content
        let parser = POFileParser::new(content.as_str().into());
        assert_eq!(parser.file.options.path_or_content, content);
        assert_eq!(parser.content_is_path, false);
        assert_eq!(parser.file.options.wrapwidth, 78);
    }

    #[test]
    fn parse_empty_file() {
        let path = "tests-data/empty.po";
        let mut parser = POFileParser::new(path.into());
        parser.parse().ok();

        assert_eq!(parser.file.entries.len(), 0);
        assert_eq!(parser.file.metadata.len(), 0);
    }

    #[test]
    fn parse_empty_content() {
        let mut parser = POFileParser::new("".into());
        parser.parse().ok();

        assert_eq!(parser.file.entries.len(), 0);
        assert_eq!(parser.file.metadata.len(), 0);
    }

    #[test]
    fn parse_utf8_bom() {
        let path = "tests-data/utf8-bom.po";
        let mut parser = POFileParser::new(path.into());
        parser.parse().ok();

        assert_eq!(
            parser.file.header,
            Some("This file contains an UTF8 BOM".to_string()),
        );
        assert_eq!(parser.file.metadata.len(), 0);
        assert_eq!(parser.file.entries.len(), 0);
    }

    #[test]
    fn parse_header() {
        let path = "tests-data/header-no-trailing-newline.po";
        let mut parser = POFileParser::new(path.into());
        parser.parse().ok();

        assert_eq!(
            parser.file.header,
            Some(
                concat!(
                    "This file is distributed under the same license",
                    "\n\n",
                    "Translators:",
                    "\n",
                    "Foo bar, Year",
                    "\n\n",
                    "Baz, YEAR",
                )
                .to_string()
            ),
        );

        let path = "tests-data/header-trailing-newlines.po";
        let mut parser = POFileParser::new(path.into());
        parser.parse().ok();

        assert_eq!(
            parser.file.header,
            Some(
                concat!(
                    "This file is distributed under the same license",
                    "\n\n",
                    "Translators:",
                    "\n",
                    "Foo bar, Year",
                    "\n\n",
                    "Baz, YEAR",
                    "\n\n\n",
                )
                .to_string()
            ),
        );

        // header must not be saved as entry
        assert_eq!(parser.file.entries.len(), 0);
    }

    #[test]
    fn parse_metadata() {
        let path = "tests-data/metadata.po";
        let mut parser = POFileParser::new(path.into());
        parser.parse().ok();

        assert_eq!(parser.file.metadata.len(), 11);

        // metadata must not be saved as entry
        assert_eq!(parser.file.entries.len(), 0);

        // check metadata
        let metadata = HashMap::from([
            ("Plural-Forms", "nplurals=2; plural=(n != 1);"),
            ("POT-Creation-Date", "2020-05-19 20:23+0200"),
            ("Content-Transfer-Encoding", "8bit"),
            ("MIME-Version", "1.0"),
            ("Report-Msgid-Bugs-To", "mondeja"),
            ("PO-Revision-Date", "2020-09-28 03:17+0000"),
            ("Project-Id-Version", "django"),
            ("Last-Translator", "Foo Bar <foobar@gmail.com>"),
            ("Content-Type", "text/plain; charset=UTF-8"),
            ("Language", "es"),
            (
                "Language-Team",
                concat!(
                    "Spanish",
                    " (http://www.transifex.com/",
                    "django/django/language/es/)",
                ),
            ),
        ]);
        for (key, value) in metadata.iter() {
            assert_eq!(
                parser.file.metadata.get(&key as &str).unwrap(),
                value
            );
        }
    }

    #[test]
    fn parse_msgids_msgstrs() {
        let path = "tests-data/msgids-msgstrs.po";
        let mut parser = POFileParser::new(path.into());
        parser.parse().ok();

        assert_eq!(parser.file.entries.len(), 2);

        let first_entry = &parser.file.entries[0];
        let second_entry = &parser.file.entries[1];

        assert_eq!(first_entry.msgid, "msgid 1");
        assert_eq!(first_entry.msgstr.as_ref().unwrap(), "msgstr 1");
        assert_eq!(first_entry.obsolete, false);

        assert_eq!(second_entry.msgid, "msgid 2");
        assert_eq!(second_entry.msgstr.as_ref().unwrap(), "msgstr 2");
        assert_eq!(second_entry.obsolete, false);
    }

    #[test]
    fn parse_long_message() {
        let path = "tests-data/long-message.po";
        let mut parser = POFileParser::new(path.into());
        parser.parse().ok();

        assert_eq!(parser.file.entries.len(), 1);

        let entry = &parser.file.entries[0];
        assert_eq!(
            entry.msgid,
            concat!(
                "Enter a valid “slug” consisting of letters, numbers,",
                " underscores or hyphens.",
            ),
        );
        assert_eq!(
            entry.msgstr.as_ref().unwrap(),
            concat!(
                "Introduzca un 'slug' válido, consistente en letras,",
                " números, guiones bajos o medios.",
            ),
        );
        assert_eq!(
            entry.comment.as_ref().unwrap(),
            "This is a generated/extracted comment",
        );
    }

    #[test]
    fn parse_long_msgids_msgstrs() {
        let path = "tests-data/msgid-msgstr-long.po";
        let mut parser = POFileParser::new(path.into());
        parser.parse().ok();

        let po_content = fs::read_to_string(path).unwrap();

        assert_eq!(parser.file.entries.len(), 2);
        assert_eq!(parser.file.metadata.len(), 0);

        let first_entry = &parser.file.entries[0];
        let second_entry = &parser.file.entries[1];

        // first entry, defined in one line at po file
        assert_eq!(
            first_entry.msgid,
            concat!(
                "msgid 1 msgid 1 msgid 1 msgid 1 msgid 1",
                " msgid 1 msgid 1 msgid 1 msgid 1 msgid 1",
                " msgid 1 msgid 1",
            )
        );
        assert_eq!(
            first_entry.msgid.len(),
            po_content.lines().nth(0).unwrap().len()
                - "msgid ".len()
                - 2,
        );

        let msgstr = first_entry.msgstr.as_ref().unwrap();
        assert_eq!(
            msgstr,
            concat!(
                "msgstr 1 msgstr 1 msgstr 1 msgstr 1 msgstr 1",
                " msgstr 1 msgstr 1 msgstr 1 msgstr 1 msgstr 1",
                " msgstr 1",
            )
        );
        assert_eq!(
            msgstr.len(),
            po_content.lines().nth(1).unwrap().len()
                - "msgstr ".len()
                - 2,
        );

        // second entry, wrapped at po file
        let expected_msgid_msgstr = concat!(
            "\\n<p class=\"help\">To install bookmarklets, drag",
            " the link to your bookmarks\\ntoolbar, or right-click",
            " the link and add it to your bookmarks. Now you can\\n",
            "select the bookmarklet from any page in the site.",
            "  Note that some of these\\nbookmarklets require you to",
            " be viewing the site from a computer designated\\n",
            "as \"internal\" (talk to your system administrator",
            " if you aren't sure if\\nyour computer is \"internal\").",
            "</p>\\n",
        );
        assert_eq!(second_entry.msgid, expected_msgid_msgstr);
        assert_eq!(
            second_entry.msgstr.as_ref().unwrap(),
            expected_msgid_msgstr
        );
    }

    #[test]
    fn parse_flags() {
        let path = "tests-data/flags.po";
        let mut parser = POFileParser::new(path.into());
        parser.parse().ok();

        assert_eq!(parser.file.entries.len(), 6);

        let entry_1 = &parser.file.entries[0];
        let entry_2 = &parser.file.entries[1];
        let entry_3 = &parser.file.entries[2];
        let entry_4 = &parser.file.entries[3];
        let entry_5 = &parser.file.entries[4];
        let entry_6 = &parser.file.entries[5];

        assert_eq!(entry_1.msgid, "msgid 1");
        assert_eq!(entry_1.msgstr.as_ref().unwrap(), "msgstr 1");
        assert_eq!(entry_1.obsolete, false);
        assert_eq!(entry_1.flags.len(), 2);
        assert_eq!(entry_1.flags, vec!["python-format", "fuzzy"]);
        assert_eq!(entry_1.fuzzy(), true);

        assert_eq!(entry_2.msgid, "msgid 2");
        assert_eq!(entry_2.msgstr.as_ref().unwrap(), "msgstr 2");
        assert_eq!(entry_2.obsolete, false);
        assert_eq!(entry_2.flags.len(), 1);
        assert_eq!(entry_2.flags[0], "fuzzy");
        assert_eq!(entry_2.fuzzy(), true);

        assert_eq!(entry_3.msgid, "msgid 3");
        assert_eq!(entry_3.msgstr.as_ref().unwrap(), "msgstr 3");
        assert_eq!(entry_3.obsolete, false);
        assert_eq!(entry_3.flags.len(), 1);
        assert_eq!(entry_3.flags[0], "python-format");
        assert_eq!(entry_3.fuzzy(), false);

        assert_eq!(entry_4.msgid, "msgid 4");
        assert_eq!(entry_4.msgstr.as_ref().unwrap(), "msgstr 4");
        assert_eq!(entry_4.obsolete, false);
        assert_eq!(entry_4.flags.len(), 7);
        assert_eq!(
            entry_4.flags,
            vec!["1", "2", "3", "4", "5", "6", "7"]
        );
        assert_eq!(entry_4.fuzzy(), false);

        assert_eq!(entry_5.msgid, "msgid 5");
        assert_eq!(entry_5.msgstr.as_ref().unwrap(), "msgstr 5");
        assert_eq!(entry_5.obsolete, false);
        assert_eq!(entry_5.flags.len(), 7);
        assert_eq!(
            entry_5.flags,
            vec!["a", "b", "c", "d", "e", "f", "g"]
        );
        assert_eq!(entry_5.fuzzy(), false);

        assert_eq!(entry_6.msgid, "msgid 6");
        assert_eq!(entry_6.msgstr.as_ref().unwrap(), "msgstr 6");
        assert_eq!(entry_6.obsolete, false);
        assert_eq!(entry_6.flags.len(), 0);
        assert_eq!(entry_6.fuzzy(), false);
    }

    #[test]
    fn parse_msgid_plural() {
        let path = "tests-data/msgid-plural.po";
        let mut parser = POFileParser::new(path.into());
        parser.parse().ok();

        assert_eq!(parser.file.entries.len(), 2);
        assert_eq!(
            parser.file.entries[0].msgid_plural.is_some(),
            true
        );
        assert_eq!(
            parser.file.entries[1].msgid_plural.is_some(),
            true
        );

        let entry_1 = &parser.file.entries[0];
        let entry_2 = &parser.file.entries[1];

        // check msgid_plural
        assert_eq!(
            entry_1.msgid_plural.as_ref().unwrap(),
            concat!(
                "A Ensure this value has at least %(limit_value)d",
                " characters (it has %(show_value)d).",
            )
        );
        assert_eq!(
            entry_2.msgid_plural.as_ref().unwrap(),
            concat!(
                "B Ensure this value has at least %(limit_value)d",
                " characters (it has %(show_value)d).",
            )
        );

        // check msgstr_plural
        assert_eq!(entry_1.msgstr_plural.len(), 2);
        assert_eq!(
            entry_1.msgstr_plural[0],
            concat!(
                "A Asegúrese de que este valor tenga al menos",
                " %(limit_value)d caracter (tiene %(show_value)d).",
            )
        );
        assert_eq!(
            entry_1.msgstr_plural[1],
            concat!(
                "A Asegúrese de que este valor tenga al menos",
                " %(limit_value)d carácter(es) (tiene%(show_value)d).",
            )
        );
    }

    #[test]
    fn parse_msgctxt() {
        let path = "tests-data/msgctxt.po";
        let mut parser = POFileParser::new(path.into());
        parser.parse().ok();

        assert_eq!(parser.file.entries.len(), 3);

        let entry_1 = &parser.file.entries[0];
        let entry_2 = &parser.file.entries[1];
        let entry_3 = &parser.file.entries[2];

        assert_eq!(entry_1.msgid, "Jan.");
        assert_eq!(entry_1.msgstr.as_ref().unwrap(), "Ene.");
        assert_eq!(
            entry_1.msgctxt.as_ref().unwrap(),
            "abbrev. month"
        );
        assert_eq!(entry_1.fuzzy(), false);

        assert_eq!(entry_2.msgid, "J.");
        assert_eq!(entry_2.msgstr.as_ref().unwrap(), "E.");
        assert_eq!(
            entry_2.msgctxt.as_ref().unwrap(),
            "abbrev. month"
        );
        assert_eq!(entry_2.fuzzy(), true);

        assert_eq!(entry_3.msgid, "To date");
        assert_eq!(
            entry_3.msgstr.as_ref().unwrap(),
            "Hasta la fecha"
        );
        assert_eq!(entry_3.msgctxt.as_ref().unwrap(), "to date");
        assert_eq!(entry_3.fuzzy(), false);
    }

    #[test]
    fn parse_previous_msgid_msgctx() {
        let path = "tests-data/previous-msgid-msgctxt.po";
        let mut parser = POFileParser::new(path.into());
        parser.parse().ok();

        assert_eq!(parser.file.entries.len(), 1);

        assert_eq!(parser.file.entries[0].msgid, "Some msgid");
        assert_eq!(
            parser.file.entries[0].previous_msgid.as_ref().unwrap(),
            "previous untranslated entry"
        );
        assert_eq!(
            parser.file.entries[0].previous_msgctxt.as_ref().unwrap(),
            "@previous_context"
        );
    }

    #[test]
    fn parse_empty_occurrences_line() {
        let path = "tests-data/empty-occurrences-line.po";
        let mut parser = POFileParser::new(path.into());
        parser.parse().ok();

        assert_eq!(parser.file.entries.len(), 1);

        let expected_occurrences = Vec::from([
            ("db/models/manipulators.py", "310"),
            ("contrib/admin/views/main.py", "342"),
            ("contrib/admin/views/main.py", "344"),
            ("contrib/admin/views/main.py", "346"),
            ("core/validators.py", "275"),
        ]);
        assert_eq!(parser.file.entries[0].occurrences.len(), 5);

        for (i, (occ_fline, occ_line)) in
            expected_occurrences.iter().enumerate()
        {
            assert_eq!(
                parser.file.entries[0].occurrences[i],
                (occ_fline.to_string(), occ_line.to_string())
            );
        }
    }

    #[test]
    fn parse_occurrence_no_linenum() {
        // Parse a occurrence line with no line number
        let path = "tests-data/occurrence-no-linenum.po";
        let mut parser = POFileParser::new(path.into());
        parser.parse().ok();

        assert_eq!(parser.file.entries.len(), 1);
        assert_eq!(parser.file.entries[0].msgid, "Hello");
        assert_eq!(
            parser.file.entries[0].msgstr.as_ref().unwrap(),
            "Bonjour"
        );
        assert_eq!(parser.file.entries[0].occurrences.len(), 2);
        assert_eq!(
            parser.file.entries[0].occurrences[0],
            ("path/to/file/noocc.rs".to_string(), "".to_string())
        );
        assert_eq!(
            parser.file.entries[0].occurrences[1],
            ("path/to/file/occ.rs".to_string(), "45".to_string())
        );
    }

    #[test]
    fn parse_weird_occurrences() {
        let path = "tests-data/weird-occurrences.po";
        let mut parser = POFileParser::new(path.into());
        parser.parse().ok();

        assert_eq!(parser.file.entries.len(), 3);

        let entry_1 = &parser.file.entries[0];
        assert_eq!(entry_1.msgid, "Windows path");
        assert_eq!(
            entry_1.occurrences,
            vec![("C:\\foo\\bar.py:12".to_string(), "".to_string())]
        );

        let entry_2 = &parser.file.entries[1];
        assert_eq!(entry_2.msgid, "Override the default prgname");
        assert_eq!(
            entry_2.occurrences,
            vec![("main.c".to_string(), "117".to_string())]
        );

        let entry_3 = &parser.file.entries[2];
        assert_eq!(entry_3.msgid, "choose new graphic");
        assert_eq!(entry_3.occurrences, vec![
            ("Balloon-Fills,BitmapFillStyle>>addFillStyleMenuItems:hand:from:".to_string(), "".to_string())
        ]);
    }

    #[test]
    fn parse_complete() {
        let path = "tests-data/django-complete.po";
        let content = fs::read_to_string(path).unwrap();

        let mut parser = POFileParser::new(content.as_str().into());
        parser.parse().ok();

        assert_eq!(parser.file.entries.len(), 341);
        assert_eq!(parser.file.header.unwrap().lines().count(), 30);

        let n_msgid_plurals = parser
            .file
            .entries
            .iter()
            .filter(|e| e.msgid_plural.is_some())
            .count();

        // check msgids
        assert_eq!(
            // -1 ignoring the header
            content.matches("msgid \"").count() - 1,
            parser.file.entries.len(),
        );

        // check msgstrs
        assert_eq!(
            // -1 ignoring the header
            content.matches("msgstr").count() - 1 - n_msgid_plurals,
            parser.file.entries.len(),
        );

        // check msgctxts
        assert_eq!(
            content.matches("msgctxt \"").count(),
            parser
                .file
                .entries
                .iter()
                .filter(|e| e.msgctxt.is_some())
                .count(),
        );

        // check plurals
        assert_eq!(
            content.matches("msgid_plural \"").count(),
            n_msgid_plurals
        );
        assert_eq!(
            content.matches("msgstr[0]").count(),
            n_msgid_plurals
        );
        assert_eq!(
            content.matches("msgstr[1]").count(),
            n_msgid_plurals
        );

        // number of flags
        assert_eq!(
            content.matches("#, ").count(),
            parser
                .file
                .entries
                .iter()
                .filter(|e| e.flags.len() > 0)
                .count(),
        );

        // number of 'python-format' flags
        assert_eq!(
            content.matches("python-format").count(),
            parser
                .file
                .entries
                .iter()
                .filter(|e| e.flags.contains(&"python-format".into()))
                .count(),
        );
    }

    #[test]
    fn parse_fuzzy_header() {
        let path = "tests-data/fuzzy-header.po";
        let mut parser = POFileParser::new(path.into());
        parser.parse().ok();

        let metadata_as_entry = parser.file.metadata_as_entry();
        assert_eq!(parser.file.entries.len(), 0);
        assert_eq!(parser.file.header.unwrap().lines().count(), 2);
        assert_eq!(metadata_as_entry.fuzzy(), true);
    }

    #[test]
    fn parse_indented() {
        let path = "tests-data/indented.po";
        let mut parser = POFileParser::new(path.into());
        parser.parse().ok();

        assert_eq!(parser.file.entries.len(), 2);
        assert_eq!(
            parser.file.entries[0].tcomment.as_ref().unwrap(),
            concat!(
                "Added for previous msgid/msgid_plural/msgctxt testing",
                "\nTokens are separated by some tabs and a single space.",
            ),
        );
    }

    #[test]
    fn parse_repeated_metadata() {
        let path = "tests-data/repeated-metadata-keys.po";
        let mut parser = POFileParser::new(path.into());
        parser.parse().ok();

        assert!(parser
            .file
            .metadata
            .contains_key("Content-Transfer-Encoding"));
        assert_eq!(
            parser
                .file
                .metadata
                .get("Content-Transfer-Encoding")
                .unwrap(),
            r"8bit4bit",
        );
        assert!(parser.file.metadata.contains_key("MIME-Version"));
        assert_eq!(
            parser.file.metadata.get("MIME-Version").unwrap(),
            "1.02.2",
        );
    }

    #[test]
    fn parse_unescaped_double_quote() {
        // on keywords
        let path = "tests-data/unescaped-double-quote-msgid.po";
        let mut parser = POFileParser::new(path.into());
        let result = parser.parse();

        assert_eq!(
            result,
            Err(SyntaxError::UnescapedDoubleQuoteFound {
                maybe_filename: MaybeFilename::new(path, true,),
                line: 5,
                index: 11,
            })
        );

        // on continuation lines
        let path =
            "tests-data/unescaped-double-quote-continuation.po";
        let mut parser = POFileParser::new(path.into());
        let result = parser.parse();

        assert_eq!(
            result,
            Err(SyntaxError::UnescapedDoubleQuoteFound {
                maybe_filename: MaybeFilename::new(path, true,),
                line: 6,
                index: 27,
            })
        );
    }

    #[test]
    fn parse_obsolete_previous_msgid() {
        let path = "tests-data/obsolete-previous-msgid.po";
        let mut parser = POFileParser::new(path.into());
        parser.parse().ok();

        assert_eq!(parser.file.entries.len(), 2);

        let obs_entry = &parser.file.entries[1];

        assert_eq!(obs_entry.obsolete, true);
        assert!(obs_entry.previous_msgid.is_none());
        assert!(obs_entry.fuzzy());
    }

    #[test]
    fn error_when_empty_previous_message_line() {
        let content = concat!(
            "#\n",
            "msgid \"\"\n",
            "msgstr \"\"\n",
            "\n",
            "#|\n",
            "msgid \"foo\"\n",
            "msgstr \"bar\"\n",
        );
        let mut parser = POFileParser::new(content.into());
        let result = parser.parse();

        assert_eq!(
            result,
            Err(SyntaxError::Custom {
                maybe_filename: MaybeFilename::new(content, false,),
                line: 5,
                index: 2,
                message: "empty previous message found".to_string(),
            })
        );
    }

    #[test]
    fn error_when_invalid_token_found() {
        let content = concat!(
            "#\n",
            "msgid \"\"\n",
            "msgstr \"\"\n",
            "\n",
            "#|msgid \"foo\"\n",
            "msgstr \"bar\"\n",
        );
        let mut parser = POFileParser::new(content.into());
        let result = parser.parse();

        assert_eq!(
            result,
            Err(SyntaxError::Generic {
                maybe_filename: MaybeFilename::new(content, false,),
                line: 5,
                index: 0,
            })
        );
    }

    #[test]
    fn error_when_non_digit_msgstr_plural_index() {
        let content = concat!(
            "#\n",
            "msgid \"\"\n",
            "msgstr \"\"\n",
            "\n",
            "msgstr[foo] \"bar\"\n",
        );
        let mut parser = POFileParser::new(content.into());
        let result = parser.parse();

        assert_eq!(
            result,
            Err(SyntaxError::Custom {
                maybe_filename: MaybeFilename::new(content, false,),
                line: 5,
                index: 7,
                message: "Invalid msgstr plural index. Expected digit, found 'foo'.".to_string(),
            })
        );
    }
}
