use base64::Encoding;
use chacha20::{ChaCha20Poly1305, KeyInit, aead::Aead};
use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent, KeyEventKind};
use ratatui::Frame;
use ratatui::layout::Alignment;
use ratatui::prelude::{Buffer, Constraint, Layout, Rect, Stylize};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span, Text};
use ratatui::widgets::{Block, BorderType, Borders, Cell, ListItem, Paragraph, Row, Table, Widget};
use zeroize::Zeroizing;

use crate::{
    App, AppEvent, DbKey, Error, Event, EventHandler, InputField, InputList, Nonce, Password,
    Result, SessionKey, Terminal, field_access,
};

mod extra;
mod state;

pub use extra::{ExtraField, ExtraFieldList, ExtraFieldListState};
pub use state::{EntryEncryption, EntryMode, EntrySelection, EntryState, EntryVisibility};

/// Represents a user entry.
#[derive(Clone, Debug)]
pub struct Entry {
    title: Zeroizing<String>,
    username: Zeroizing<String>,
    password: Zeroizing<String>,
    totp: Option<totp::TOTP>,
    state: EntryState,
    session_key: SessionKey,
    session_nonce: Nonce,
    extras: ExtraFieldList,
    db_key: DbKey,
}

impl Entry {
    /// Length of the encoded entry length field.
    ///
    /// Database entry fields are encoded as:
    ///
    /// ```text,no_run,no_build
    /// [LENGTH {32-bit}][ENTRY {LENGTH-bytes}]
    /// ```
    pub const LENGTH_LEN: usize = std::mem::size_of::<u32>();
    /// Represents hidden password string.
    pub const HIDDEN_CONTENT: &str = "********";
    /// Represents an emtpy TOTP token.
    pub const NO_TOTP: &str = "      ";
    /// Represents the number of columns when displayed in a table.
    pub const COLUMNS: usize = 4;
    /// Gets the database key for the TOTP secret field.
    pub const TOTP_KEY: &str = "totp";
    /// Represents the number of required input fields.
    pub const REQUIRED_FIELDS: usize = 4;

    /// Creates a new [Entry].
    pub fn new() -> Self {
        Self {
            title: Zeroizing::new(String::new()),
            username: Zeroizing::new(String::new()),
            password: Zeroizing::new(String::new()),
            totp: None,
            state: EntryState::new(),
            session_key: SessionKey::random(),
            session_nonce: Nonce::counter(),
            extras: ExtraFieldList::new(),
            db_key: DbKey::new(),
        }
    }

    /// Gets whether the entry is empty.
    pub fn is_empty(&self) -> bool {
        self.title.is_empty()
    }

    /// Renders the password based on the [EntryState].
    pub fn render_password(&self) -> &str {
        match self.state.visibility() {
            EntryVisibility::Show => self.password.as_str(),
            EntryVisibility::Hide => Self::HIDDEN_CONTENT,
        }
    }

    /// Gets the number of columns when displayed in a table.
    #[inline]
    pub const fn columns() -> usize {
        Self::COLUMNS
    }

    #[inline]
    fn style(&self) -> Style {
        match self.state.selection() {
            EntrySelection::Selected => Style::default().fg(Color::Yellow),
            EntrySelection::Unselected => Style::default(),
        }
    }

    /// Gets a reference to the TOTP instance.
    pub fn totp(&self) -> Option<&totp::TOTP> {
        self.totp.as_ref()
    }

    /// Sets the TOTP instance.
    ///
    /// The `totp` secret should be the Base32-encoded secret string.
    pub fn set_totp(&mut self, totp: &str) -> Result<()> {
        Self::create_totp(totp).map(|t| self.totp = Some(t))
    }

    /// Builder function that sets the TOTP instance.
    ///
    /// The `totp` secret should be the Base32-encoded secret string.
    pub fn with_totp(self, totp: &str) -> Result<Self> {
        Self::create_totp(totp).map(|t| Self {
            totp: Some(t),
            ..self
        })
    }

    /// Creates a RFC6238 TOTP from the provided secret.
    ///
    /// The `secret` should be the Base32-encoded secret string.
    pub fn create_totp(secret: &str) -> Result<totp::TOTP> {
        totp::Secret::Encoded(secret.into())
            .to_bytes()
            .map_err(Error::from)
            .and_then(|s| totp::Rfc6238::with_defaults(s).map_err(Error::from))
            .and_then(|rfc| totp::TOTP::from_rfc6238(rfc).map_err(Error::from))
    }

    /// Gets the TOTP secret.
    ///
    /// Returns the raw secret bytes.
    pub fn totp_secret(&self) -> Option<&[u8]> {
        self.totp.as_ref().map(|t| t.secret.as_slice())
    }

    /// Gets the TOTP secret.
    ///
    /// Returns the Base32-encoded secret string.
    pub fn totp_secret_encoded(&self) -> Option<Zeroizing<String>> {
        use base32::Encoding;

        self.totp
            .as_ref()
            .map(|t| t.secret.as_slice())
            .map(base32::Base32UpperUnpadded::encode_string)
            .map(Zeroizing::new)
    }

    /// Gets the current TOTP token.
    pub fn totp_token(&self) -> Result<String> {
        self.totp
            .as_ref()
            .ok_or(Error::io_other("missing TOTP"))
            .and_then(|t| t.generate_current().map_err(Error::from))
    }

    /// Renders the next TOTP token.
    ///
    /// Generates an empty string if no TOTP is configured.
    pub fn render_totp(&self) -> String {
        self.totp_token()
            .map(|t| format!(" | {t}"))
            .unwrap_or_default()
    }

    /// Gets a mutable reference to the extra field list.
    pub fn extras_mut(&mut self) -> &mut ExtraFieldList {
        &mut self.extras
    }

    /// Converts the [Entry] to a TUI list item.
    pub fn as_list_item(&self) -> ListItem<'_> {
        ListItem::new(
            Line::from(vec![
                Span::styled(self.title(), Style::default().add_modifier(Modifier::BOLD)),
                Span::raw(" | "),
                Span::raw(self.username()),
                Span::raw(" | "),
                Span::raw(self.render_password()),
                Span::raw(self.render_totp()),
            ])
            .style(self.style()),
        )
    }

    /// Converts the [Entry] to a TUI row.
    pub fn as_row(&self) -> Row<'_> {
        Row::new(vec![
            Cell::new(self.title()).style(Style::default().bold()),
            Cell::new(self.username()),
            Cell::new(self.render_password()),
            Cell::new(self.totp_token().unwrap_or_default()),
        ])
        .style(self.style())
    }

    /// Gets whether the entry is encrypted.
    pub fn is_encrypted(&self) -> bool {
        self.state.is_encrypted()
    }

    /// Sets the entry selection state.
    pub fn set_selection(&mut self, selection: EntrySelection) {
        self.state.set_selection(selection);
    }

    /// Sets the entry visibility.
    pub fn set_visibility(&mut self, visibility: EntryVisibility) -> Result<()> {
        match (self.state.visibility(), visibility) {
            (EntryVisibility::Hide, EntryVisibility::Show) => self.session_decrypt(),
            (EntryVisibility::Show, EntryVisibility::Hide) => self.session_encrypt(),
            (EntryVisibility::Show, EntryVisibility::Show)
            | (EntryVisibility::Hide, EntryVisibility::Hide) => Ok(()),
        }
    }

    /// Toggles the entry visibility.
    pub fn toggle_visibility(&mut self) -> Result<()> {
        match self.state.visibility() {
            EntryVisibility::Show => self.session_encrypt(),
            EntryVisibility::Hide => self.session_decrypt(),
        }
    }

    /// Reveals the session-encrypted password.
    pub fn reveal_password(&mut self) -> Result<Zeroizing<String>> {
        let encrypted = self.is_encrypted();

        if encrypted {
            self.session_decrypt()?;
        }

        let password = self.password.clone();

        if encrypted {
            self.session_encrypt()?;
        }

        Ok(password)
    }

    /// Encrypts the entry for storage in the database.
    ///
    /// Encodes the [Entry] using length-prefix encoding:
    ///
    /// ```text,no_build,no_run
    /// [FIELD_LEN {32-bits}][FIELD {FIELD_LEN-bytes}]
    /// ```
    pub fn encrypt(&self, key: &Password, nonce: &Nonce) -> Result<Zeroizing<Vec<u8>>> {
        let cipher = ChaCha20Poly1305::new_from_slice(key.as_ref())?;

        let t = Self::serialize_field(self.title.as_str());
        let u = Self::serialize_field(self.username.as_str());
        let p = Self::serialize_field(self.password.as_str());

        let mut val = Zeroizing::new(t.chain(u).chain(p).collect::<Vec<u8>>());

        if let Some(totp) = self.totp_secret_encoded() {
            val.extend(Self::serialize_field(Self::TOTP_KEY));
            val.extend(Self::serialize_field(totp.as_str()));
        }

        for field in self.extras.items().iter() {
            val.extend(Self::serialize_field(field.title()));
            val.extend(Self::serialize_field(field.content()));
        }

        cipher
            .encrypt(nonce.as_nonce(), val.as_slice())
            .map(Zeroizing::new)
            .map_err(Error::from)
    }

    /// Attempts to decrypt an encrypted [Entry].
    ///
    /// `cipher_text` should be the encrypted, length-prefix encoded [Entry] with the Poly1305 MAC.
    pub fn decrypt(key: &Password, nonce: &Nonce, cipher_text: &[u8]) -> Result<Self> {
        let cipher = ChaCha20Poly1305::new_from_slice(key.as_ref())?;

        let entry_bytes = cipher
            .decrypt(nonce.as_nonce(), cipher_text)
            .map(Zeroizing::new)
            .map_err(|err| Error::io_crypto(format!("error decrypting entry: {err}")))?;

        let (t, title_end) = Self::deserialize_field(entry_bytes.as_slice(), 0)
            .map_err(|err| Error::io_db(format!("invalid title field: {err}")))?;

        let (u, username_end) = entry_bytes
            .get(title_end..)
            .ok_or(Error::io_db("missing username"))
            .and_then(|remaining| Self::deserialize_field(remaining, title_end))
            .map_err(|err| Error::io_db(format!("invalid username field: {err}")))?;

        let (p, password_end) = entry_bytes
            .get(username_end..)
            .ok_or(Error::io_db("missing password"))
            .and_then(|remaining| Self::deserialize_field(remaining, username_end))
            .map_err(|err| Error::io_db(format!("invalid password field: {err}")))?;

        Self::new()
            .with_title(t)
            .with_username(u)
            .with_password(p)
            .deserialize_extra_fields(&entry_bytes, password_end)
    }

    /// Attempts to deserialize optional extra fields (including TOTP secret).
    fn deserialize_extra_fields(mut self, entry_bytes: &[u8], mut end: usize) -> Result<Self> {
        let mut extras = ExtraFieldList::new();

        while let Some(remaining) = entry_bytes.get(end..)
            && !remaining.is_empty()
        {
            let (field_title, title_end) = Self::deserialize_field(remaining, end)
                .map_err(|err| Error::io_db(format!("invalid extra field title: {err}")))?;

            let remaining = entry_bytes
                .get(title_end..)
                .ok_or(Error::io_db("missing extra field content"))?;

            let (field_content, content_end) = Self::deserialize_field(remaining, title_end)
                .map_err(|err| Error::io_db(format!("invalid extra field content: {err}")))?;

            end = content_end;

            if field_title == Self::TOTP_KEY && self.totp().is_none() {
                self.set_totp(field_content)?;
            } else {
                let field = ExtraField::new()
                    .with_title(field_title)
                    .with_content(field_content);

                extras.push(field);
            }
        }

        if !extras.is_empty() {
            self.set_extras(extras);
        }

        Ok(self)
    }

    /// Serializes a field into a length-prefix encoding.
    fn serialize_field(field: &str) -> impl Iterator<Item = u8> {
        (field.len() as u32)
            .to_le_bytes()
            .into_iter()
            .chain(field.as_bytes().iter().copied())
    }

    /// Deserializes a length-prefix encoded field.
    ///
    /// Returns the field passwords, and the field's ending position.
    fn deserialize_field(buf: &[u8], start_pos: usize) -> Result<(&str, usize)> {
        let field_start = Self::LENGTH_LEN;

        let buf_len = buf.len();

        let len = buf
            .get(..field_start)
            .ok_or(Error::io_db(format!(
                "invalid field length, start: {start_pos}, length: {buf_len}"
            )))
            .and_then(|b| b.try_into().map_err(Error::from))
            .map(u32::from_le_bytes)?;

        let field_end = field_start.saturating_add(len as usize);

        buf.get(field_start..field_end)
            .ok_or(Error::io_db("field length exceeds buffer length"))
            .and_then(|b| str::from_utf8(b).map_err(Error::from))
            .map_err(|err| Error::io_db(format!("invalid field: {err}")))
            .map(|s| (s, start_pos.saturating_add(field_end)))
    }

    /// Encrypts protected entry password with a session key.
    pub fn session_encrypt(&mut self) -> Result<()> {
        if self.state.is_encrypted() {
            return Err(Error::io_crypto("entry is already encrypted"));
        }

        let cipher = ChaCha20Poly1305::new_from_slice(self.session_key.as_ref())?;
        self.session_nonce.increment()?;

        let encrypted_password = cipher
            .encrypt(self.session_nonce.as_nonce(), self.password.as_bytes())
            .map(Zeroizing::new)
            .map(|d| base64::Base64::encode_string(d.as_slice()))
            .map(Zeroizing::new)?;

        self.password = encrypted_password;

        for field in self.extras.items_mut().iter_mut() {
            field.session_encrypt()?;
        }

        self.state.set_visibility(EntryVisibility::Hide);
        self.state.set_encryption(EntryEncryption::Encrypted);

        Ok(())
    }

    /// Decrypts protected entry password with a session key.
    pub fn session_decrypt(&mut self) -> Result<()> {
        if !self.state.is_encrypted() {
            return Err(Error::io_crypto("entry is already decrypted"));
        }

        let cipher = ChaCha20Poly1305::new_from_slice(self.session_key.as_ref())?;

        let decoded = base64::Base64::decode_vec(self.password.as_str()).map(Zeroizing::new)?;

        let decrypted_password = cipher
            .decrypt(self.session_nonce.as_nonce(), decoded.as_slice())
            .map_err(Error::from)
            .and_then(|d| String::from_utf8(d).map_err(Error::from))
            .map(Zeroizing::new)?;

        self.password = decrypted_password;

        for field in self.extras.items_mut().iter_mut() {
            field.session_decrypt()?;
        }

        self.state.set_visibility(EntryVisibility::Show);
        self.state.set_encryption(EntryEncryption::Decrypted);

        Ok(())
    }

    /// Gets the [Entry] vertical constraints.
    pub(crate) fn vertical_constraints(&self) -> impl Iterator<Item = Constraint> {
        let help = 3;
        let main_entry = Self::REQUIRED_FIELDS.saturating_mul(2).saturating_add(1) as u16;
        let extras_len = self.extras.len().saturating_mul(2).saturating_add(1) as u16;

        [
            Constraint::Length(help),
            Constraint::Length(main_entry),
            Constraint::Length(extras_len),
        ]
        .into_iter()
    }

    /// Draws the [Entry] as a TUI frame.
    pub(crate) fn draw(&mut self, frame: &mut Frame) {
        frame.render_widget(self, frame.area());
    }

    /// Adds an [Entry] to the database.
    pub async fn add_to_db(
        terminal: &mut Terminal,
        events: &mut EventHandler,
        db: &sled::Db,
        password: &Password,
        db_nonce: &mut Nonce,
        db_key: DbKey,
    ) -> Result<Option<Self>> {
        let res = InputList::new()
            .with_items(vec![
                InputField::create("title"),
                InputField::create("username"),
                InputField::create_hidden("password"),
                InputField::create_hidden("totp"),
            ])
            .run(terminal, events)
            .await?;

        let Ok([t, u, p, totp]) = res
            .get(..Self::REQUIRED_FIELDS)
            .ok_or(Error::io_db("missing required entry fields"))
            .map(|i| i.iter().map(|s| s.as_str()).collect::<Vec<_>>())
            .and_then(|i| {
                <[&str; Self::REQUIRED_FIELDS]>::try_from(i.as_slice()).map_err(Error::from)
            })
        else {
            return Ok(None);
        };

        let mut entry = Self::new().with_title(t).with_username(u).with_password(p);

        if !totp.is_empty() {
            entry.set_totp(totp)?;
        }

        let consumed = Self::REQUIRED_FIELDS.saturating_div(ExtraField::COLUMNS);

        entry.extras = res
            .chunks_exact(ExtraField::COLUMNS)
            .skip(consumed)
            .map(|input| {
                ExtraField::new()
                    .with_title(input[0].as_str())
                    .with_content(input[1].as_str())
            })
            .collect::<Vec<_>>()
            .into();

        db_nonce.increment()?;

        let db_entry = entry
            .encrypt(password, db_nonce)
            .map(|cipher_text| {
                db_nonce
                    .as_ref()
                    .iter()
                    .chain(cipher_text.iter())
                    .cloned()
                    .collect::<Vec<u8>>()
            })
            .map(Zeroizing::new)?;

        db.insert(db_key.as_ref(), db_entry.as_slice())
            .map_err(Error::from)
            .and_then(|_| entry.session_encrypt())?;

        entry.set_db_key(db_key);

        Ok(Some(entry))
    }

    /// Runs the [Entry] event loop.
    pub(crate) async fn run(
        &mut self,
        terminal: &mut Terminal,
        events: &mut EventHandler,
        db: &sled::Db,
        password: &Password,
        db_nonce: &mut Nonce,
    ) -> Result<()> {
        loop {
            terminal.draw(|frame| self.draw(frame))?;

            if self.state.mode() == EntryMode::Editing {
                self.handle_editing(terminal, events, db, password, db_nonce)
                    .await?;
            }

            match events.next().await? {
                Event::Crossterm(CrosstermEvent::Key(key)) if key.kind == KeyEventKind::Press => {
                    if self.state.mode() == EntryMode::Viewing
                        && let Some(ret) = self.handle_viewing_key(key)?
                    {
                        return Ok(ret);
                    }
                }
                Event::App(AppEvent::ClearClipboard) => App::clear_clipboard().await?,
                _ => {}
            }
        }
    }

    /// Handles a key event in normal mode.
    pub(crate) fn handle_viewing_key(&mut self, key: KeyEvent) -> Result<Option<()>> {
        match key.code {
            KeyCode::Enter | KeyCode::Esc | KeyCode::Char('q') => {
                return Ok(Some(()));
            }
            KeyCode::Char('e') | KeyCode::Char('i') => {
                self.state.set_mode(EntryMode::Editing);
            }
            KeyCode::Char('r') => self.toggle_visibility()?,
            _ => {}
        }

        Ok(None)
    }

    /// Handles editing mode.
    pub(crate) async fn handle_editing(
        &mut self,
        terminal: &mut Terminal,
        events: &mut EventHandler,
        db: &sled::Db,
        password: &Password,
        db_nonce: &mut Nonce,
    ) -> Result<()> {
        let encrypted = self.state.is_encrypted();

        if encrypted {
            self.session_decrypt()?;
        }

        let mut inputs = vec![
            InputField::create("title").with_input(self.title()),
            InputField::create("username").with_input(self.username()),
            InputField::create_hidden("password").with_input(self.password()),
            InputField::create_hidden("totp")
                .with_input(self.totp_secret_encoded().unwrap_or_default().as_str()),
        ];

        let fields = self.extras().items().iter().flat_map(|field| {
            [
                InputField::create("field title").with_input(field.title()),
                InputField::create_hidden("field content").with_input(field.content()),
            ]
            .into_iter()
        });

        inputs.extend(fields);

        let res = InputList::new()
            .with_items(inputs)
            .run(terminal, events)
            .await?;

        self.update(&res)?;

        db_nonce.increment()?;

        let db_entry = self
            .encrypt(password, db_nonce)
            .map(|cipher_text| {
                db_nonce
                    .as_ref()
                    .iter()
                    .chain(cipher_text.iter())
                    .cloned()
                    .collect::<Vec<u8>>()
            })
            .map(Zeroizing::new)?;

        let db_key = self.db_key();

        db.insert(db_key.as_ref(), db_entry.as_slice())?;

        if encrypted {
            self.session_encrypt()?;
        }

        self.state.set_mode(EntryMode::Viewing);

        Ok(())
    }

    /// Updates the [Entry] with the results of an input field list.
    pub(crate) fn update(&mut self, res: &[String]) -> Result<()> {
        let Ok([t, u, p, totp]) = res
            .get(..Self::REQUIRED_FIELDS)
            .ok_or(Error::io_db("missing required edit fields"))
            .map(|i| i.iter().map(|s| s.as_str()).collect::<Vec<_>>())
            .and_then(|i| {
                <[&str; Self::REQUIRED_FIELDS]>::try_from(i.as_slice()).map_err(Error::from)
            })
        else {
            return Ok(());
        };

        self.set_title(t);
        self.set_username(u);
        self.set_password(p);

        if !totp.is_empty() {
            self.set_totp(totp)?;
        }

        let mut consumed = Self::REQUIRED_FIELDS.saturating_div(ExtraField::COLUMNS);

        if res.len() > consumed {
            res.chunks_exact(ExtraField::COLUMNS)
                .skip(consumed)
                .zip(self.extras_mut().items_mut())
                .for_each(|(input, field)| {
                    field.set_title(input[0].as_str());
                    field.set_content(input[1].as_str());
                    consumed = consumed.saturating_add(1);
                });

            if res.len() > consumed {
                res.chunks_exact(ExtraField::COLUMNS)
                    .skip(consumed)
                    .for_each(|input| {
                        let extra = ExtraField::new()
                            .with_title(input[0].as_str())
                            .with_content(input[1].as_str());

                        self.extras_mut().push(extra);
                    });
            }
        }

        Ok(())
    }

    /// Converts the [ExtraFieldList] into a TUI widget.
    pub fn as_widget(&self) -> Table<'_> {
        let fields = [
            Row::new([Cell::new("Title")]).style(Style::default().bold().blue()),
            Row::new([Cell::new(self.title())]),
            Row::new([Cell::new("Username")]).style(Style::default().bold().blue()),
            Row::new([Cell::new(self.username())]),
            Row::new([Cell::new("Password")]).style(Style::default().bold().blue()),
            Row::new([Cell::new(self.render_password())]),
            Row::new([Cell::new("TOTP")]).style(Style::default().bold().blue()),
            Row::new([Cell::new(self.totp_token().unwrap_or_default())]),
        ];

        Table::new(fields, [Constraint::Fill(1)])
    }

    /// Renders the [Entry] TUI help message.
    pub fn help_message(&self) -> Paragraph<'_> {
        let msg = vec![
            "Press ".into(),
            "Enter".bold(),
            " or ".into(),
            "Esc".bold(),
            " or ".into(),
            "q".bold(),
            " to exit | ".into(),
            "i".bold(),
            " or ".into(),
            "e".bold(),
            " to start editing | ".into(),
            "r".bold(),
            " to show/hide protected content".into(),
        ];

        let style = Style::default();

        Paragraph::new(Text::from(Line::from(msg).centered()).patch_style(style))
            .block(App::title_block())
    }
}

field_access! {
    Entry {
        /// The entry title.
        title: zeroize_cow { str },
        /// The entry username.
        username: zeroize_cow { str },
        /// The entry password.
        password: zeroize_cow { str },
    }
}

field_access! {
    Entry {
        /// Indicates whether to show/hide the entry password.
        state: EntryState,
    }
}

field_access! {
    Entry {
        /// List of extra fields with custom titles.
        extras: as_ref { ExtraFieldList },
        /// Key used to index the entry in the database.
        db_key: as_ref { DbKey },
    }
}

impl Default for Entry {
    fn default() -> Self {
        Self::new()
    }
}

impl PartialEq for Entry {
    fn eq(&self, oth: &Self) -> bool {
        self.title == oth.title
            && self.username == oth.username
            && self.password == oth.password
            && self.totp == oth.totp
            && self.state == oth.state
            && self.extras == oth.extras
    }
}

impl Widget for &mut Entry {
    fn render(self, area: Rect, buf: &mut Buffer) {
        let constraints = self.vertical_constraints();

        Layout::vertical(constraints)
            .split(area)
            .iter()
            .enumerate()
            .for_each(|(i, &area)| {
                if i == 0 {
                    self.help_message().render(area, buf);
                } else if i == 1 {
                    let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
                    let [title_area, entry_area] = vertical.areas(area);

                    Block::bordered()
                        .title(" Main Entry ")
                        .title_alignment(Alignment::Center)
                        .title_style(Style::default().bold().magenta())
                        .border_type(BorderType::Thick)
                        .borders(Borders::TOP)
                        .render(title_area, buf);

                    self.as_widget().render(entry_area, buf);
                } else if !self.extras.is_empty() {
                    let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
                    let [title_area, fields_area] = vertical.areas(area);

                    Block::bordered()
                        .title(" Extra Fields ")
                        .title_alignment(Alignment::Center)
                        .title_style(Style::default().bold().magenta())
                        .border_type(BorderType::Thick)
                        .borders(Borders::TOP)
                        .render(title_area, buf);

                    self.extras.as_widget(area).render(fields_area, buf);
                }
            });
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{App, Salt};

    const RAND_ENTRIES: usize = 1024;
    const RAND_ENTRY_LEN: usize = 128;

    #[test]
    fn test_entry() {
        let title = "test title";
        let password = "test password";

        let mut entry = Entry::new().with_title(title).with_password(password);
        entry.session_encrypt().unwrap();

        assert_eq!(entry.title(), title);
        assert!(entry.password() != password);
        assert_eq!(
            base64::Base64::decode_vec(entry.password()).unwrap().len(),
            password.len() + App::MAC_LEN
        );

        assert_eq!(entry.state().visibility(), EntryVisibility::Hide);
        assert_eq!(entry.render_password(), Entry::HIDDEN_CONTENT);

        entry.toggle_visibility().unwrap();

        assert_eq!(entry.state().visibility(), EntryVisibility::Show);
        assert_eq!(entry.render_password(), password);

        let mut entries = Vec::with_capacity(RAND_ENTRIES);
        let mut nonces = Vec::with_capacity(RAND_ENTRIES);

        let password = Password::hash(b"test password", &Salt::random());

        (0..RAND_ENTRIES).for_each(|_| {
            let mut title = String::from("test title");
            (0..RAND_ENTRY_LEN).for_each(|_| title.push(rand::random::<char>()));

            let mut username = String::from("test username");
            (0..RAND_ENTRY_LEN).for_each(|_| username.push(rand::random::<char>()));

            let mut pass = String::from("test password");
            (0..RAND_ENTRY_LEN).for_each(|_| pass.push(rand::random::<char>()));

            let nonce = Nonce::random();
            let mut entry = Entry::new()
                .with_title(title)
                .with_username(username)
                .with_password(&pass);

            // ensure we have a unique Nonce and Entry
            assert!(!nonces.contains(&nonce));
            assert!(!entries.contains(&entry));

            let cipher_text = entry.encrypt(&password, &nonce).unwrap();
            assert_eq!(
                Entry::decrypt(&password, &nonce, cipher_text.as_slice()).unwrap(),
                entry
            );

            entry.session_encrypt().unwrap();
            assert_eq!(
                base64::Base64::decode_vec(entry.password()).unwrap().len(),
                pass.len() + App::MAC_LEN
            );
            assert!(entry.password() != pass);

            entry.session_decrypt().unwrap();
            assert_eq!(entry.password(), pass);

            nonces.push(nonce);
            entries.push(entry);
        });
    }
}
