// termui.rs

// *************************************************************************
// * Copyright (C) 2017-2019 Daniel Mueller (deso@posteo.net)              *
// *                                                                       *
// * This program is free software: you can redistribute it and/or modify  *
// * it under the terms of the GNU General Public License as published by  *
// * the Free Software Foundation, either version 3 of the License, or     *
// * (at your option) any later version.                                   *
// *                                                                       *
// * This program is distributed in the hope that it will be useful,       *
// * but WITHOUT ANY WARRANTY; without even the implied warranty of        *
// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
// * GNU General Public License for more details.                          *
// *                                                                       *
// * You should have received a copy of the GNU General Public License     *
// * along with this program.  If not, see <http://www.gnu.org/licenses/>. *
// *************************************************************************

use std::any::Any;
use std::io::Result;
use std::path::PathBuf;

use gui::derive::Widget;
use gui::Handleable;
use gui::Id;
use gui::MutCap;
use gui::UiEvent;
use gui::UiEvents;

use crate::query::Query;
use crate::state::State;
use crate::state::TaskState;
use crate::state::UiState;
use crate::tasks::Id as TaskId;
#[cfg(all(test, not(feature = "readline")))]
use crate::tasks::Task;

use super::event::Event;
use super::event::Key;
use super::in_out::InOut;
use super::in_out::InOutArea;
use super::tab_bar::IterationState;
use super::tab_bar::SearchState;
use super::tab_bar::TabBar;
use super::tab_bar::TabState;


/// An enumeration comprising all custom events we support.
#[derive(Debug)]
pub enum TermUiEvent {
  /// An event to ask a widget to select the task with the given
  /// `TaskId`.
  SelectTask(TaskId, IterationState),
  /// Search for a task containing the given string in its summary and
  /// select it.
  SearchTask(String, SearchState, IterationState),
  /// The tab with the given `Id` has selected the task as indicated by
  /// `SelectTask` or `SearchTask`.
  SelectedTask(Id),
  /// Set the state of the input/output area.
  SetInOut(InOut),
  /// Change the state of the input/output area to Clear, unless the
  /// generation ID supplied does not match the current generation ID.
  /// This event is internal to the InOutArea, there is no need for
  /// other clients to use it.
  ClearInOut(usize),
  /// Text has been entered.
  EnteredText(String),
  /// Text input has been canceled.
  InputCanceled,
  /// An event used to collect the state from the `TabBar`.
  CollectState(Id),
  /// The response to the `CollectState` event.
  CollectedState(Vec<(Query, Option<usize>)>, Option<usize>),
  /// An event used to collect the state of all tabs.
  GetTabState(TabState, IterationState),
  /// A indication that some component changed and that we should
  /// re-render everything.
  Updated,
  /// Retrieve the current set of tasks.
  #[cfg(all(test, not(feature = "readline")))]
  GetTasks,
  /// The response to the `GetTasks` event.
  #[cfg(all(test, not(feature = "readline")))]
  GotTasks(Vec<Task>),
  /// Retrieve the current state of the input/output area.
  #[cfg(all(test, not(feature = "readline")))]
  GetInOut,
  /// The response to the `GetInOut` event.
  #[cfg(all(test, not(feature = "readline")))]
  GotInOut(InOut),
}

impl TermUiEvent {
  /// Check whether the event is the `Updated` variant.
  pub fn is_updated(&self) -> bool {
    if let TermUiEvent::Updated = self {
      true
    } else {
      false
    }
  }
}


/// An implementation of a terminal based view.
#[derive(Debug, Widget)]
#[gui(Event = "Event")]
pub struct TermUi {
  id: Id,
  in_out: Id,
  tab_bar: Id,
  task_state: TaskState,
  ui_state_path: PathBuf,
}


impl TermUi {
  /// Create a new view associated with the given `State` object.
  pub fn new(id: Id, cap: &mut dyn MutCap<Event>, state: State) -> Self {
    let State(task_state, UiState{path, queries, selected}) = state;
    let mut queries = Some(queries);

    let in_out = cap.add_widget(id, &mut |id, cap| {
      Box::new(InOutArea::new(id, cap))
    });
    let tab_bar = cap.add_widget(id, &mut |id, cap| {
      let queries = queries.take().unwrap();
      Box::new(TabBar::new(id, cap, &task_state, queries, selected))
    });

    TermUi {
      id: id,
      in_out: in_out,
      tab_bar: tab_bar,
      task_state: task_state,
      ui_state_path: path,
    }
  }

  /// Persist the state into a file.
  fn save_all(&self, ui_state: &UiState) -> Result<()> {
    self.task_state.save()?;
    // TODO: We risk data inconsistencies if the second save operation
    //       fails.
    ui_state.save()?;
    Ok(())
  }

  /// Save the current state.
  fn save_and_report(&mut self, ui_state: &UiState) -> UiEvents<Event> {
    let in_out = match self.save_all(ui_state) {
      Ok(_) => InOut::Saved,
      Err(err) => InOut::Error(format!("{}", err)),
    };
    let event = TermUiEvent::SetInOut(in_out);
    UiEvent::Directed(self.in_out, Box::new(event)).into()
  }

  /// Emit an event that will eventually cause the state to be saved.
  fn save(&mut self) -> UiEvents<Event> {
    let event = TermUiEvent::CollectState(self.id);
    UiEvent::Directed(self.tab_bar, Box::new(event)).into()
  }

  /// Handle a custom event.
  fn handle_custom_event(&mut self, event: Box<TermUiEvent>) -> Option<UiEvents<Event>> {
    match *event {
      TermUiEvent::CollectedState(queries, selected) => {
        let ui_state = UiState {
          path: self.ui_state_path.clone(),
          queries: queries,
          selected: selected,
        };
        Some(self.save_and_report(&ui_state))
      },
      TermUiEvent::SetInOut(_) => {
        Some(UiEvent::Directed(self.in_out, event).into())
      },
      #[cfg(all(test, not(feature = "readline")))]
      TermUiEvent::GetTasks => {
        let tasks = self.task_state.tasks();
        let tasks = tasks.borrow().iter().cloned().collect();
        let resp = TermUiEvent::GotTasks(tasks);
        Some(UiEvent::Custom(Box::new(resp)).into())
      },
      #[cfg(all(test, not(feature = "readline")))]
      TermUiEvent::GetInOut => {
        // We merely relay this event to the InOutArea widget, which is
        // the only entity able to satisfy the request.
        Some(UiEvent::Directed(self.in_out, event).into())
      },
      _ => Some(UiEvent::Custom(event).into()),
    }
  }
}

impl Handleable<Event> for TermUi {
  /// Check for new input and react to it.
  fn handle(&mut self, event: Event, _cap: &mut dyn MutCap<Event>) -> Option<UiEvents<Event>> {
    match event {
      Event::Key(key, _) => {
        match key {
          Key::Char('q') => Some(UiEvent::Quit.into()),
          Key::Char('w') => Some(self.save()),
          _ => Some(event.into()),
        }
      },
    }
  }

  /// Handle a custom event.
  fn handle_custom(&mut self,
                   event: Box<dyn Any>,
                   _cap: &mut dyn MutCap<Event>) -> Option<UiEvents<Event>> {
    match event.downcast::<TermUiEvent>() {
      Ok(e) => self.handle_custom_event(e),
      Err(e) => panic!("Received unexpected custom event: {:?}", e),
    }
  }
}


// We can't run the "end-to-end" tests in conjunction with readline
// support. Readline can be configured outside of this program's control
// and so key bindings could be arbitrary.
#[cfg(all(test, not(feature = "readline")))]
#[allow(unused_results)]
mod tests {
  use super::*;

  use gui::Ui;
  use gui::UnhandledEvent;
  use gui::UnhandledEvents;

  use crate::ser::query::Query as SerQuery;
  use crate::ser::query::TagLit as SerTagLit;
  use crate::ser::state::TaskState as SerTaskState;
  use crate::ser::state::UiState as SerUiState;
  use crate::ser::tags::Templates as SerTemplates;
  use crate::ser::tasks::Task as SerTask;
  use crate::ser::tasks::Tasks as SerTasks;
  use crate::ser::ToSerde;
  use crate::state::State;
  use crate::test::make_tasks;
  use crate::test::make_tasks_with_tags;
  use crate::test::NamedTempFile;
  use crate::ui::event::EventUpdated;
  use crate::ui::event::tests::CustomEvent;


  impl From<Key> for Event {
    fn from(key: Key) -> Self {
      Event::Key(key, ())
    }
  }

  impl From<char> for Event {
    fn from(c: char) -> Self {
      Event::Key(Key::Char(c), ())
    }
  }


  /// Create the default `UiState` with four queries and 15 tasks with
  /// tags. Tag assignment follows the pattern that
  /// `make_tasks_with_tags` creates.
  fn default_tasks_and_tags() -> (SerTaskState, SerUiState) {
    let (tags, templates, tasks) = make_tasks_with_tags(15);
    let task_state = SerTaskState {
      templates: SerTemplates(templates),
      tasks: SerTasks(tasks),
    };
    let ui_state = SerUiState {
      queries: vec![
        (SerQuery {
          name: "all".to_string(),
          lits: vec![],
        }, None),
        (SerQuery {
          name: "tag complete".to_string(),
          lits: vec![vec![SerTagLit::Pos(tags[0])]],
        }, None),
        (SerQuery {
          name: "tag2 || tag3".to_string(),
          lits: vec![
            vec![
              SerTagLit::Pos(tags[2]),
              SerTagLit::Pos(tags[3]),
            ],
          ],
        }, None),
        (SerQuery {
          name: "tag1 && tag3".to_string(),
          lits: vec![
            vec![SerTagLit::Pos(tags[1])],
            vec![SerTagLit::Pos(tags[3])],
          ],
        }, None),
      ],
      selected: None,
    };

    (task_state, ui_state)
  }

  /// A builder object used for instantiating a UI with a certain
  /// composition of tasks.
  struct TestUiBuilder {
    task_state: SerTaskState,
    ui_state: SerUiState,
  }

  impl TestUiBuilder {
    /// Create a builder that will create a UI without any tasks.
    fn new() -> TestUiBuilder {
      TestUiBuilder {
        task_state: Default::default(),
        ui_state: Default::default(),
      }
    }

    /// Create a builder that will create a UI with the given tasks.
    fn with_ser_tasks<T>(tasks: T) -> TestUiBuilder
    where
      T: AsRef<[SerTask]> + Into<Vec<SerTask>>,
    {
      tasks.as_ref().iter().for_each(|x| assert!(x.tags.is_empty()));

      TestUiBuilder {
        task_state: SerTaskState {
          templates: Default::default(),
          tasks: SerTasks(tasks.into()),
        },
        ui_state: Default::default(),
      }
    }

    /// Create a builder that will instantiate a UI with state as
    /// created by `default_tasks_and_tags`.
    fn with_default_tasks_and_tags() -> TestUiBuilder {
      let (task_state, ui_state) = default_tasks_and_tags();
      TestUiBuilder { task_state, ui_state }
    }

    /// Build the actual UI object that we can test with.
    fn build(self) -> TestUi {
      let mut task_state = Some(self.task_state);
      let mut ui_state = Some(self.ui_state);
      let task_file = NamedTempFile::new();
      let ui_file = NamedTempFile::new();

      let (ui, _) = Ui::new(&mut |id, cap| {
        let task_state = task_state.take().unwrap();
        let ui_state = ui_state.take().unwrap();
        let state = State::with_serde(task_state, task_file.path(), ui_state, ui_file.path());
        Box::new(TermUi::new(id, cap, state.unwrap()))
      });

      TestUi {
        task_file: task_file,
        ui_file: ui_file,
        ui: ui,
      }
    }
  }

  /// An UI object used for testing. It is just a handy wrapper around a
  /// `Ui`.
  #[allow(unused)]
  struct TestUi {
    task_file: NamedTempFile,
    ui_file: NamedTempFile,
    ui: Ui<Event>,
  }

  impl TestUi {
    /// Handle a single event and directly return the result.
    fn evaluate<E>(&mut self, event: E) -> Option<UnhandledEvents<Event>>
    where
      E: Into<UiEvent<Event>>,
    {
      self.ui.handle(event)
    }

    /// Send the given list of events to the UI.
    fn handle(&mut self, events: Vec<UiEvent<Event>>) -> &mut Self {
      for event in events.into_iter() {
        if let Some(event) = self.ui.handle(event) {
          if let UnhandledEvent::Quit = event.into_last() {
            break
          }
        }
      }
      self
    }

    /// Retrieve the current `InOutArea` state.
    fn in_out(&mut self) -> InOut {
      let event = UiEvent::Custom(Box::new(TermUiEvent::GetInOut));
      let resp = self
        .ui
        .handle(event)
        .unwrap()
        .unwrap_custom::<TermUiEvent>();

      if let TermUiEvent::GotInOut(in_out) = resp {
        in_out
      } else {
        panic!("Unexpected response: {:?}", resp)
      }
    }

    /// Retrieve the current set of tasks from the UI.
    fn tasks(&mut self) -> Vec<Task> {
      let event = UiEvent::Custom(Box::new(TermUiEvent::GetTasks));
      let resp = self
        .ui
        .handle(event)
        .unwrap()
        .unwrap_custom::<TermUiEvent>();

      if let TermUiEvent::GotTasks(tasks) = resp {
        tasks
      } else {
        panic!("Unexpected response: {:?}", resp)
      }
    }

    /// Retrieve the current set of tasks in the form of `SerTask` objects from the UI.
    fn ser_tasks(&mut self) -> Vec<SerTask> {
      self.tasks().iter().map(|x| x.to_serde()).collect()
    }

    /// Load the UI's state from a file. Note that unless the state has
    /// been saved, the result will probably just be the default state.
    fn load_state(&self) -> Result<State> {
      State::new(self.task_file.path(), self.ui_file.path())
    }
  }

  #[test]
  fn exit_on_quit() {
    let events = vec![
      Event::from('q').into(),
      Event::from('a').into(),
      Event::from('f').into(),
      Event::from('\n').into(),
    ];

    let tasks = TestUiBuilder::new()
      .build()
      .handle(events)
      .ser_tasks();

    assert_eq!(tasks, make_tasks(0))
  }

  #[test]
  fn remove_no_task() {
    let events = vec![
      Event::from('d').into(),
    ];

    let tasks = TestUiBuilder::new()
      .build()
      .handle(events)
      .ser_tasks();

    assert_eq!(tasks, make_tasks(0))
  }

  #[test]
  fn remove_only_task() {
    let tasks = make_tasks(1);
    let events = vec![
      Event::from('d').into(),
    ];

    let tasks = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .ser_tasks();

    assert_eq!(tasks, make_tasks(0))
  }

  #[test]
  fn remove_task_after_down_select() {
    let tasks = make_tasks(2);
    let events = vec![
      Event::from('j').into(),
      Event::from('j').into(),
      Event::from('j').into(),
      Event::from('j').into(),
      Event::from('j').into(),
      Event::from('d').into(),
    ];

    let tasks = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .ser_tasks();

    assert_eq!(tasks, make_tasks(1))
  }

  #[test]
  fn remove_task_after_up_select() {
    let tasks = make_tasks(3);
    let events = vec![
      Event::from('j').into(),
      Event::from('j').into(),
      Event::from('k').into(),
      Event::from('d').into(),
    ];

    let tasks = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .ser_tasks();

    let mut expected = make_tasks(3);
    expected.remove(1);
    assert_eq!(tasks, expected)
  }

  #[test]
  fn remove_last_task() {
    let tasks = make_tasks(3);
    let events = vec![
      Event::from('j').into(),
      Event::from('j').into(),
      Event::from('d').into(),
      Event::from('k').into(),
      Event::from('d').into(),
    ];

    let tasks = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .ser_tasks();

    let mut expected = make_tasks(3);
    expected.remove(0);
    expected.remove(1);
    assert_eq!(tasks, expected)
  }

  #[test]
  fn remove_second_to_last_task() {
    let tasks = make_tasks(5);
    let events = vec![
      Event::from('G').into(),
      Event::from('k').into(),
      Event::from('d').into(),
    ];

    let tasks = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .ser_tasks();

    let mut expected = make_tasks(5);
    expected.remove(3);
    assert_eq!(tasks, expected)
  }

  #[test]
  fn remove_second_task_after_up_select() {
    let tasks = make_tasks(3);
    let events = vec![
      Event::from('k').into(),
      Event::from('k').into(),
      Event::from('k').into(),
      Event::from('k').into(),
      Event::from('k').into(),
      Event::from('k').into(),
      Event::from('j').into(),
      Event::from('d').into(),
    ];

    let tasks = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .ser_tasks();

    let mut expected = make_tasks(3);
    expected.remove(1);
    assert_eq!(tasks, expected)
  }

  #[test]
  fn selection_after_removal() {
    let tasks = make_tasks(5);
    let events = vec![
      Event::from('G').into(),
      Event::from('d').into(),
      Event::from('e').into(),
      Event::from(Key::Backspace).into(),
      Event::from('o').into(),
      Event::from('\n').into(),
    ];

    let tasks = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .ser_tasks();

    let mut expected = make_tasks(4);
    expected[3].summary = "o".to_string();
    assert_eq!(tasks, expected)
  }

  #[test]
  fn add_task() {
    let events = vec![
      Event::from('a').into(),
      Event::from('f').into(),
      Event::from('o').into(),
      Event::from('o').into(),
      Event::from('b').into(),
      Event::from('a').into(),
      Event::from('r').into(),
      Event::from('\n').into(),
    ];

    let tasks = TestUiBuilder::new()
      .build()
      .handle(events)
      .ser_tasks();

    let mut expected = make_tasks(1);
    expected[0].summary = "foobar".to_string();

    assert_eq!(tasks, expected)
  }

  #[test]
  fn add_and_remove_tasks() {
    let events = vec![
      Event::from('a').into(),
      Event::from('f').into(),
      Event::from('o').into(),
      Event::from('o').into(),
      Event::from('\n').into(),
      Event::from('a').into(),
      Event::from('b').into(),
      Event::from('a').into(),
      Event::from('r').into(),
      Event::from('\n').into(),
      Event::from('d').into(),
      Event::from('d').into(),
    ];

    let tasks = TestUiBuilder::new()
      .build()
      .handle(events)
      .ser_tasks();

    assert_eq!(tasks, make_tasks(0))
  }

  #[test]
  fn add_task_cancel() {
    let events = vec![
      Event::from('a').into(),
      Event::from('f').into(),
      Event::from('o').into(),
      Event::from('o').into(),
      Event::from('b').into(),
      Event::from('a').into(),
      Event::from('z').into(),
      Event::from(Key::Esc).into(),
    ];

    let tasks = TestUiBuilder::new()
      .build()
      .handle(events)
      .ser_tasks();

    assert_eq!(tasks, make_tasks(0))
  }

  #[test]
  fn add_task_with_character_removal() {
    let tasks = make_tasks(1);
    let events = vec![
      Event::from('a').into(),
      Event::from('f').into(),
      Event::from('o').into(),
      Event::from('o').into(),
      Event::from(Key::Backspace).into(),
      Event::from(Key::Backspace).into(),
      Event::from(Key::Backspace).into(),
      Event::from('b').into(),
      Event::from('a').into(),
      Event::from('z').into(),
      Event::from('\n').into(),
    ];

    let tasks = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .ser_tasks();

    let mut expected = make_tasks(2);
    expected[1].summary = "baz".to_string();

    assert_eq!(tasks, expected)
  }

  #[test]
  fn add_task_with_cursor_movement() {
    let tasks = make_tasks(1);
    let events = vec![
      Event::from('a').into(),
      Event::from(Key::Delete).into(),
      Event::from(Key::Right).into(),
      Event::from('s').into(),
      Event::from('t').into(),
      Event::from(Key::Home).into(),
      Event::from(Key::Home).into(),
      Event::from('t').into(),
      Event::from('e').into(),
      Event::from(Key::End).into(),
      Event::from(Key::End).into(),
      Event::from(Key::Delete).into(),
      Event::from('4').into(),
      Event::from('2').into(),
      Event::from('\n').into(),
    ];

    let tasks = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .ser_tasks();

    let mut expected = make_tasks(2);
    expected[1].summary = "test42".to_string();

    assert_eq!(tasks, expected)
  }

  #[test]
  fn add_empty_task() {
    let tasks = make_tasks(1);
    let events = vec![
      Event::from('a').into(),
      Event::from('\n').into(),
    ];

    let tasks = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .ser_tasks();

    assert_eq!(tasks, make_tasks(1))
  }

  #[test]
  fn add_task_from_completed_one() {
    let events = vec![
      Event::from('l').into(),
      Event::from('a').into(),
      Event::from('t').into(),
      Event::from('e').into(),
      Event::from('s').into(),
      Event::from('t').into(),
      Event::from('\n').into(),
      Event::from('l').into(),
      Event::from('l').into(),
      Event::from('g').into(),
      Event::from('a').into(),
      Event::from('h').into(),
      Event::from('i').into(),
      Event::from('\n').into(),
    ];

    let tasks = TestUiBuilder::with_default_tasks_and_tags()
      .build()
      .handle(events)
      .tasks();

    assert_eq!(tasks[15].summary, "test");

    let tags = tasks[15]
      .tags()
      .map(|x| x.name())
      .collect::<Vec<_>>();
    let expected = Vec::<String>::new();
    assert_eq!(tags, expected);

    assert_eq!(tasks[16].summary, "hi");

    let tags = tasks[16]
      .tags()
      .map(|x| x.name())
      .collect::<Vec<_>>();
    let expected = vec!["tag2"];
    assert_eq!(tags, expected);
  }

  #[test]
  fn add_task_with_tags() {
    let events = vec![
      Event::from('l').into(),
      Event::from('l').into(),
      Event::from('l').into(),
      Event::from('a').into(),
      Event::from('f').into(),
      Event::from('o').into(),
      Event::from('o').into(),
      Event::from('\n').into(),
    ];

    let tasks = TestUiBuilder::with_default_tasks_and_tags()
      .build()
      .handle(events)
      .tasks();

    assert_eq!(tasks[15].summary, "foo");

    let tags = tasks[15]
      .tags()
      .map(|x| x.name())
      .collect::<Vec<_>>();
    let expected = vec![
      "tag1".to_string(),
      "tag2".to_string(),
      "tag3".to_string(),
    ];
    assert_eq!(tags, expected);
  }

  #[test]
  fn complete_task() {
    let tasks = make_tasks(3);
    let events = vec![
      Event::from('j').into(),
      Event::from(' ').into(),
      Event::from('j').into(),
      Event::from(' ').into(),
      Event::from(' ').into(),
    ];

    let tasks = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .tasks();

    assert!(!tasks[0].is_complete());
    assert!(tasks[1].is_complete());
    assert!(!tasks[2].is_complete());
  }

  #[test]
  fn edit_task() {
    let tasks = make_tasks(3);
    let events = vec![
      Event::from('j').into(),
      Event::from('e').into(),
      Event::from(Key::Backspace).into(),
      Event::from('a').into(),
      Event::from('m').into(),
      Event::from('e').into(),
      Event::from('n').into(),
      Event::from('d').into(),
      Event::from('\n').into(),
    ];

    let tasks = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .ser_tasks();

    let mut expected = make_tasks(3);
    expected[1].summary = "amend".to_string();

    assert_eq!(tasks, expected);
  }

  #[test]
  fn edit_task_cancel() {
    let tasks = make_tasks(3);
    let events = vec![
      Event::from('j').into(),
      Event::from('e').into(),
      Event::from(Key::Esc).into(),
      Event::from('a').into(),
      Event::from('f').into(),
      Event::from('o').into(),
      Event::from('o').into(),
      Event::from('\n').into(),
    ];

    let tasks = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .ser_tasks();

    let mut expected = make_tasks(4);
    expected[3].summary = "foo".to_string();

    assert_eq!(tasks, expected);
  }

  #[test]
  fn edit_task_to_empty() {
    let tasks = make_tasks(3);
    let events = vec![
      Event::from('j').into(),
      Event::from('e').into(),
      Event::from(Key::Backspace).into(),
      Event::from('\n').into(),
    ];

    let tasks = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .ser_tasks();

    let mut expected = make_tasks(3);
    expected.remove(1);

    assert_eq!(tasks, expected)
  }

  #[test]
  fn edit_task_multi_byte_char() {
    let tasks = make_tasks(1);
    let events = vec![
      Event::from('e').into(),
      Event::from('ä').into(),
      Event::from('ö').into(),
      Event::from('\n').into(),
    ];

    let tasks = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .ser_tasks();

    assert_eq!(tasks, make_tasks(1))
  }

  #[test]
  fn edit_task_with_cursor_movement() {
    let tasks = make_tasks(3);
    let events = vec![
      Event::from('j').into(),
      Event::from('j').into(),
      Event::from('e').into(),
      Event::from(Key::Left).into(),
      Event::from(Key::Left).into(),
      Event::from(Key::Left).into(),
      Event::from(Key::Delete).into(),
      Event::from('t').into(),
      Event::from('a').into(),
      Event::from(Key::Left).into(),
      Event::from(Key::Delete).into(),
      Event::from(Key::Right).into(),
      Event::from('e').into(),
      Event::from('s').into(),
      Event::from('t').into(),
      Event::from(Key::Right).into(),
      Event::from(Key::Right).into(),
      Event::from('\n').into(),
    ];

    let tasks = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .ser_tasks();

    let mut expected = make_tasks(3);
    expected[2].summary = "test".to_string();

    assert_eq!(tasks, expected);
  }

  #[test]
  fn edit_multiple_tasks() {
    let events = vec![
      Event::from('3').into(),
      Event::from('e').into(),
      Event::from('a').into(),
      Event::from('\n').into(),
      Event::from('e').into(),
      Event::from('b').into(),
      Event::from('\n').into(),
    ];

    let tasks = TestUiBuilder::with_default_tasks_and_tags()
      .build()
      .handle(events)
      .tasks()
      .into_iter()
      .map(|x| x.summary)
      .collect::<Vec<_>>();

    let (.., mut expected) = make_tasks_with_tags(15);
    expected[8].summary = "9ab".to_string();
    let expected = expected.into_iter().map(|x| x.summary).collect::<Vec<_>>();

    assert_eq!(tasks, expected);
  }

  #[test]
  fn move_no_or_one_task_up_or_down() {
    fn test_tasks(count: usize, key: char) {
      let tasks = make_tasks(count);
      let events = vec![
        Event::from(key).into(),
      ];

      let tasks = TestUiBuilder::with_ser_tasks(tasks)
        .build()
        .handle(events)
        .ser_tasks();

      assert_eq!(tasks, make_tasks(count));
    }

    test_tasks(0, 'J');
    test_tasks(1, 'J');
    test_tasks(0, 'K');
    test_tasks(1, 'K');
  }

  #[test]
  fn move_task_down() {
    let tasks = make_tasks(3);
    let events = vec![
      Event::from('J').into(),
    ];

    let tasks = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .ser_tasks();

    let mut expected = make_tasks(3);
    expected.swap(0, 1);

    assert_eq!(tasks, expected);
  }

  #[test]
  fn move_task_up() {
    let tasks = make_tasks(3);
    let events = vec![
      Event::from('G').into(),
      Event::from('K').into(),
    ];

    let tasks = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .ser_tasks();

    let mut expected = make_tasks(3);
    expected.swap(2, 1);

    assert_eq!(tasks, expected);
  }

  #[test]
  fn move_second_task_down() {
    let tasks = make_tasks(4);
    let events = vec![
      Event::from('j').into(),
      Event::from('J').into(),
    ];

    let tasks = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .ser_tasks();

    let mut expected = make_tasks(4);
    expected.swap(1, 2);

    assert_eq!(tasks, expected);
  }

  #[test]
  fn move_second_task_up() {
    let tasks = make_tasks(4);
    let events = vec![
      Event::from('j').into(),
      Event::from('K').into(),
    ];

    let tasks = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .ser_tasks();

    let mut expected = make_tasks(4);
    expected.swap(1, 0);

    assert_eq!(tasks, expected);
  }

  #[test]
  fn transparent_task_removal_down_to_empty_query() {
    let events = vec![
      Event::from('l').into(),
      Event::from('l').into(),
      Event::from('l').into(),
      Event::from('G').into(),
      Event::from('h').into(),
      Event::from('h').into(),
      Event::from('h').into(),
      Event::from('G').into(),
      Event::from('d').into(),
      Event::from('l').into(),
      Event::from('l').into(),
      Event::from('l').into(),
      // The edit should be a no-op.
      Event::from('e').into(),
      Event::from('\n').into(),
    ];

    let tasks = TestUiBuilder::with_default_tasks_and_tags()
      .build()
      .handle(events)
      .tasks();

    assert_eq!(tasks.len(), 14);
  }

  #[test]
  fn tab_selection_by_number() {
    let events = vec![
      Event::from('4').into(),
      Event::from('e').into(),
      Event::from('a').into(),
      Event::from('\n').into(),
    ];

    let tasks = TestUiBuilder::with_default_tasks_and_tags()
      .build()
      .handle(events)
      .tasks()
      .into_iter()
      .map(|x| x.summary)
      .collect::<Vec<_>>();

    let (.., mut expected) = make_tasks_with_tags(15);
    expected[14].summary = "15a".to_string();
    let expected = expected
      .into_iter()
      .map(|x| x.summary)
      .collect::<Vec<_>>();

    assert_eq!(tasks, expected);
  }

  #[test]
  fn select_previous_tab() {
    let events = vec![
      Event::from('2').into(),
      Event::from('e').into(),
      Event::from('a').into(),
      Event::from('\n').into(),
      Event::from('4').into(),
      Event::from('e').into(),
      Event::from('a').into(),
      Event::from('\n').into(),
      Event::from('`').into(),
      Event::from('j').into(),
      Event::from('e').into(),
      Event::from('a').into(),
      Event::from('\n').into(),
    ];

    let tasks = TestUiBuilder::with_default_tasks_and_tags()
      .build()
      .handle(events)
      .tasks()
      .into_iter()
      .map(|x| x.summary)
      .collect::<Vec<_>>();

    let (.., mut expected) = make_tasks_with_tags(15);
    expected[1].summary = "2a".to_string();
    expected[3].summary = "4a".to_string();
    expected[14].summary = "15a".to_string();
    let expected = expected
      .into_iter()
      .map(|x| x.summary)
      .collect::<Vec<_>>();

    assert_eq!(tasks, expected);
  }

  #[test]
  fn select_last_tab_plus_one() {
    let events = vec![
      Event::from('0').into(),
      Event::from('l').into(),
      Event::from('d').into(),
    ];

    let tasks = TestUiBuilder::with_default_tasks_and_tags()
      .build()
      .handle(events)
      .tasks()
      .into_iter()
      .map(|x| x.summary)
      .collect::<Vec<_>>();

    let (.., mut expected) = make_tasks_with_tags(15);
    expected.remove(14);
    let expected = expected
      .into_iter()
      .map(|x| x.summary)
      .collect::<Vec<_>>();

    assert_eq!(tasks, expected);
  }

  #[test]
  fn in_out_state_after_write() {
    let tasks = make_tasks(2);
    let events = vec![
      Event::from('w').into(),
    ];

    let state = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .in_out();

    assert_eq!(state, InOut::Saved);
  }

  #[test]
  fn in_out_state_after_write_and_key_press() {
    fn with_key(key: impl Into<Event>) -> InOut {
      let tasks = make_tasks(2);
      let events = vec![
        Event::from('w').into(),
        key.into().into(),
      ];

      TestUiBuilder::with_ser_tasks(tasks)
        .build()
        .handle(events)
        .in_out()
    }

    // We test all ASCII chars.
    for c in 0u8..127u8 {
      let c = c as char;
      if c != 'a' && c != 'e' && c != 'n' && c != 'N' && c != 'w' && c != '/' && c != '?' {
        assert_eq!(with_key(c), InOut::Clear, "char: {} ({})", c, c as u8);
      }
    }

    assert_eq!(with_key(Key::Esc), InOut::Clear);
    assert_eq!(with_key('\n'), InOut::Clear);
    assert_eq!(with_key(Key::PageDown), InOut::Clear);
  }

  #[test]
  fn in_out_state_on_edit() {
    fn with_key(key: impl Into<Event>) -> InOut {
      let tasks = make_tasks(4);
      let events = vec![
        Event::from('a').into(),
        key.into().into(),
      ];

      TestUiBuilder::with_ser_tasks(tasks)
        .build()
        .handle(events)
        .in_out()
    }

    for c in 0u8..127u8 {
      if c == b'\n' {
        continue
      }

      let state = with_key(c);
      match state {
        InOut::Input(_, _) => (),
        _ => assert!(false, "Unexpected state {:?} for char {}", state, c),
      }
    }

    assert_eq!(with_key(Key::Esc), InOut::Clear);
    assert_eq!(with_key('\n'), InOut::Clear);
  }

  #[test]
  fn search_abort() {
    fn test(c: char) {
      let tasks = make_tasks(4);
      let events = vec![
        Event::from(c).into(),
        Event::from('f').into(),
        Event::from(Key::Esc).into(),
      ];

      let state = TestUiBuilder::with_ser_tasks(tasks)
        .build()
        .handle(events)
        .in_out();

      assert_eq!(state, InOut::Clear);
    }

    test('/');
    test('?');
  }

  #[test]
  fn search_tasks_not_found() {
    fn test(c: char) {
      let tasks = make_tasks(8);
      let events = vec![
        Event::from(c).into(),
        Event::from('f').into(),
        Event::from('o').into(),
        Event::from('o').into(),
        Event::from('\n').into(),
      ];

      let state = TestUiBuilder::with_ser_tasks(tasks)
        .build()
        .handle(events)
        .in_out();

      let expected = InOut::Error("Text 'foo' not found".to_string());
      assert_eq!(state, expected);
    }

    test('/');
    test('?');
  }

  #[test]
  fn search_multiple_tabs_not_found() {
    fn test(c: char) {
      let events = vec![
        Event::from(c).into(),
        Event::from('f').into(),
        Event::from('o').into(),
        Event::from('o').into(),
        Event::from('\n').into(),
      ];

      let state = TestUiBuilder::with_default_tasks_and_tags()
        .build()
        .handle(events)
        .in_out();

      let expected = InOut::Error("Text 'foo' not found".to_string());
      assert_eq!(state, expected);
    }

    test('/');
    test('?');
  }

  #[test]
  fn search_continue_without_start() {
    fn test(c: char) {
      let tasks = make_tasks(4);
      let events = vec![
        Event::from(c).into(),
        Event::from(c).into(),
        Event::from(c).into(),
      ];

      let state = TestUiBuilder::with_ser_tasks(tasks)
        .build()
        .handle(events)
        .in_out();

      let expected = InOut::Error("Nothing to search for".to_string());
      assert_eq!(state, expected);
    }

    test('n');
    test('N');
  }

  #[test]
  fn search_tasks_repeatedly_not_found() {
    fn test(c: char) {
      let tasks = make_tasks(8);
      let events = vec![
        Event::from('/').into(),
        Event::from('z').into(),
        Event::from('\n').into(),
        Event::from(c).into(),
      ];

      let state = TestUiBuilder::with_ser_tasks(tasks)
        .build()
        .handle(events)
        .in_out();

      let expected = InOut::Error("Text 'z' not found".to_string());
      assert_eq!(state, expected);
    }

    test('n');
    test('N');
  }

  #[test]
  fn search_tasks() {
    let tasks = make_tasks(12);
    let events = vec![
      Event::from('/').into(),
      Event::from('2').into(),
      Event::from('\n').into(),
      Event::from('d').into(),
      Event::from('n').into(),
      Event::from('d').into(),
    ];

    let tasks = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .ser_tasks();

    let mut expected = make_tasks(12);
    expected.remove(11);
    expected.remove(1);

    assert_eq!(tasks, expected);
  }

  #[test]
  fn search_reverse() {
    let tasks = make_tasks(12);
    let events = vec![
      Event::from('?').into(),
      Event::from('2').into(),
      Event::from('\n').into(),
      Event::from('d').into(),
    ];

    let tasks = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .ser_tasks();

    let mut expected = make_tasks(12);
    expected.remove(11);

    assert_eq!(tasks, expected);
  }

  #[test]
  fn search_case_insensitive() {
    fn test(c: char) {
      let tasks = vec![
        SerTask {
          summary: "First".to_string(),
          tags: Default::default(),
        },
        SerTask {
          summary: "SeCOnd".to_string(),
          tags: Default::default(),
        },
      ];
      let events = vec![
        Event::from(c).into(),
        Event::from('c').into(),
        Event::from('o').into(),
        Event::from('N').into(),
        Event::from('d').into(),
        Event::from('\n').into(),
        Event::from('d').into(),
      ];

      let tasks = TestUiBuilder::with_ser_tasks(tasks)
        .build()
        .handle(events)
        .ser_tasks();

      let expected = vec![
        SerTask {
          summary: "First".to_string(),
          tags: Default::default(),
        },
      ];
      assert_eq!(tasks, expected);
    }

    test('/');
    test('?');
  }

  #[test]
  fn search_starting_at_selection() {
    let tasks = make_tasks(15);
    let events = vec![
      Event::from('j').into(),
      Event::from('j').into(),
      Event::from('j').into(),
      Event::from('j').into(),
      Event::from('j').into(),
      Event::from('/').into(),
      Event::from('1').into(),
      Event::from('\n').into(),
      Event::from('d').into(),
    ];

    let tasks = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .ser_tasks();

    let mut expected = make_tasks(15);
    expected.remove(9);

    assert_eq!(tasks, expected);
  }

  #[test]
  fn search_overlapping() {
    let tasks = make_tasks(5);
    let events = vec![
      Event::from('G').into(),
      Event::from('/').into(),
      Event::from('2').into(),
      Event::from('\n').into(),
      Event::from('d').into(),
    ];

    let tasks = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .ser_tasks();

    let mut expected = make_tasks(5);
    expected.remove(1);

    assert_eq!(tasks, expected);
  }

  #[test]
  fn search_tasks_on_multiple_tabs() {
    fn test(c1: char, c2: char) {
      let events = vec![
        // Switch to tag2 || tag3 tab.
        Event::from('l').into(),
        // Search for a task with '4' in it.
        Event::from(c1).into(),
        Event::from('4').into(),
        Event::from('\n').into(),
        // Delete it. That should be task 14.
        Event::from('d').into(),
        // Move to next task.
        Event::from(c2).into(),
        // Delete it. That should be task 4.
        Event::from('d').into(),
      ];

      let tasks = TestUiBuilder::with_default_tasks_and_tags()
        .build()
        .handle(events)
        .tasks()
        .into_iter()
        .map(|x| x.summary)
        .collect::<Vec<_>>();

      let (.., mut expected) = make_tasks_with_tags(15);
      expected.remove(13);
      expected.remove(3);
      let expected = expected.into_iter().map(|x| x.summary).collect::<Vec<_>>();

      assert_eq!(tasks, expected);
    }

    test('/', 'n');
    test('/', 'N');
    test('?', 'n');
    test('?', 'N');
  }

  #[test]
  fn search_wrap_around() {
    let events = vec![
      Event::from('/').into(),
      Event::from('2').into(),
      // After this event we should have selected task '2'.
      Event::from('\n').into(),
      // After this event task '12'.
      Event::from('n').into(),
      // Rename.
      Event::from('e').into(),
      Event::from(Key::Backspace).into(),
      Event::from(Key::Backspace).into(),
      Event::from('a').into(),
      Event::from('a').into(),
      Event::from('\n').into(),
      // '2'
      Event::from('n').into(),
      // '2'
      Event::from('n').into(),
      Event::from('d').into(),
    ];

    let tasks = TestUiBuilder::with_default_tasks_and_tags()
      .build()
      .handle(events)
      .tasks()
      .into_iter()
      .map(|x| x.summary)
      .collect::<Vec<_>>();

    let (.., mut expected) = make_tasks_with_tags(15);
    expected[11].summary = "aa".to_string();
    expected.remove(1);
    let expected = expected.into_iter().map(|x| x.summary).collect::<Vec<_>>();

    assert_eq!(tasks, expected);
  }

  #[test]
  fn valid_update_events() {
    for c in 0u8..127u8 {
      let mut ui = TestUiBuilder::new().build();
      let updated = ui
        .evaluate(Event::from(c))
        .map_or(false, |x| x.is_updated());

      let c = c as char;
      let expected = c == '/' || c == '?' || c == 'a' || c == 'n' || c == 'N' || c == 'w';
      assert_eq!(updated, expected, "char: {} ({})", c, c as u8);
    }
  }

  #[test]
  fn search_no_update_without_change() {
    let mut ui = TestUiBuilder::new().build();
    let updated = ui
      .evaluate(Event::from('n'))
      .map_or(false, |x| x.is_updated());

    assert!(updated);

    let updated = ui
      .evaluate(Event::from('n'))
      .map_or(false, |x| x.is_updated());

    assert!(!updated);
  }

  #[test]
  fn save_ui_state_single_tab() {
    let tasks = make_tasks(1);
    let events = vec![
      Event::from('w').into(),
    ];
    let state = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .load_state()
      .unwrap()
      .1
      .to_serde();

    let expected = SerUiState {
      queries: vec![
        (SerQuery {
          name: "all".to_string(),
          lits: vec![],
        }, Some(0)),
      ],
      selected: Some(0),
    };
    assert_eq!(state, expected)
  }

  #[test]
  fn save_ui_state_single_tab_different_task() {
    let tasks = make_tasks(5);
    let events = vec![
      Event::from('j').into(),
      Event::from('j').into(),
      Event::from('w').into(),
    ];
    let state = TestUiBuilder::with_ser_tasks(tasks)
      .build()
      .handle(events)
      .load_state()
      .unwrap()
      .1
      .to_serde();

    let expected = SerUiState {
      queries: vec![
        (SerQuery {
          name: "all".to_string(),
          lits: vec![],
        }, Some(2)),
      ],
      selected: Some(0),
    };
    assert_eq!(state, expected)
  }

  #[test]
  fn save_ui_state_multiple_tabs() {
    let events = vec![
      Event::from('w').into(),
    ];
    let state = TestUiBuilder::with_default_tasks_and_tags()
      .build()
      .handle(events)
      .load_state()
      .unwrap()
      .1
      .to_serde();

    let (_, expected) = default_tasks_and_tags();
    assert_eq!(state.queries.len(), expected.queries.len());
    assert_eq!(state.queries.len(), 4);
    assert_eq!(state.queries[0].0.name, expected.queries[0].0.name);
    assert_eq!(state.queries[1].0.name, expected.queries[1].0.name);
    assert_eq!(state.queries[2].0.name, expected.queries[2].0.name);
    assert_eq!(state.queries[3].0.name, expected.queries[3].0.name);
    assert_eq!(state.selected, Some(0));
  }

  #[test]
  fn save_ui_state_after_various_changes() {
    let events = vec![
      Event::from('j').into(),
      Event::from('l').into(),
      Event::from('j').into(),
      Event::from('j').into(),
      Event::from('l').into(),
      Event::from('j').into(),
      Event::from('j').into(),
      Event::from('j').into(),
      Event::from('j').into(),
      Event::from('w').into(),
    ];
    let state = TestUiBuilder::with_default_tasks_and_tags()
      .build()
      .handle(events)
      .load_state()
      .unwrap()
      .1
      .to_serde();

    let (_, expected) = default_tasks_and_tags();
    assert_eq!(state.queries.len(), expected.queries.len());
    assert_eq!(state.queries.len(), 4);
    assert_eq!(state.queries[0].0.name, expected.queries[0].0.name);
    assert_eq!(state.queries[1].0.name, expected.queries[1].0.name);
    assert_eq!(state.queries[2].0.name, expected.queries[2].0.name);
    assert_eq!(state.queries[3].0.name, expected.queries[3].0.name);
    assert_eq!(state.queries[0].1, Some(1));
    assert_eq!(state.queries[1].1, Some(2));
    assert_eq!(state.queries[2].1, Some(4));
    assert_eq!(state.queries[3].1, Some(0));
    assert_eq!(state.selected, Some(2));
  }
}
