require "spec_helper"

describe Imap::Backup::Configuration::Account do
  class MockHighlineMenu
    attr_reader :choices
    attr_accessor :header

    def initialize
      @choices = {}
    end

    def choice(name, &block)
      choices[name] = block
    end

    def hidden(name, &block)
      choices[name] = block
    end
  end

  context "#initialize" do
    let(:store) { "store" }
    let(:account) { "account" }
    let(:highline) { "highline" }

    subject { described_class.new(store, account, highline) }

    [:store, :account, :highline].each do |param|
      it "expects #{param}" do
        expect(subject.send(param)).to eq(send(param))
      end
    end
  end

  context "#run" do
    let(:highline) { double("Highline") }
    let(:menu) { MockHighlineMenu.new }
    let(:store) { double("Imap::Backup::Configuration::Store", accounts: accounts) }
    let(:accounts) { [account, account1] }
    let(:account) do
      {
        username: existing_email,
        server: existing_server,
        local_path: "/backup/path",
        folders: [{name: "my_folder"}],
        password: existing_password,
      }
    end
    let(:account1) do
      {
        username: other_email,
        local_path: other_existing_path,
      }
    end
    let(:existing_email) { "user@example.com" }
    let(:new_email) { "foo@example.com" }
    let(:existing_server) { "imap.example.com" }
    let(:existing_password) { "password" }
    let(:other_email) { "other@example.com" }
    let(:other_existing_path) { "/other/existing/path" }

    before do
      allow(subject).to receive(:system).and_return(nil)
      allow(subject).to receive(:puts).and_return(nil)
      allow(highline).to receive(:choose) do |&block|
        block.call(menu)
        throw :done
      end
    end

    subject { described_class.new(store, account, highline) }

    context "preparation" do
      before { subject.run }

      it "clears the screen" do
        expect(subject).to have_received(:system).with("clear")
      end

      context "menu" do
        it "shows the menu" do
          expect(highline).to have_received(:choose)
        end
      end
    end

    context "menu" do
      [
        "modify email",
        "modify password",
        "modify server",
        "modify backup path",
        "choose backup folders",
        "test connection",
        "delete",
        "return to main menu",
        "quit", # TODO: quit is hidden
      ].each do |item|
        before { subject.run }

        it "has a '#{item}' item" do
          expect(menu.choices).to include(item)
        end
      end
    end

    context "account details" do
      [
        ["email", /email:\s+user@example.com/],
        ["server", /server:\s+imap.example.com/],
        ["password", /password:\s+x+/],
        ["path", %r(path:\s+/backup/path)],
        ["folders", /folders:\s+my_folder/]
      ].each do |attribute, value|
        before { subject.run }

        it "shows the #{attribute}" do
          expect(menu.header).to match(value)
        end
      end

      context "with no password" do
        let(:existing_password) { "" }

        before { subject.run }

        it "indicates that a password is not set" do
          expect(menu.header).to include("password: (unset)")
        end
      end
    end

    context "email" do
      before do
        allow(Imap::Backup::Configuration::Asker).to receive(:email).and_return(new_email)
        subject.run
        menu.choices["modify email"].call
      end

      context "if the server is blank" do
        [
          ["GMail", "foo@gmail.com", "imap.gmail.com"],
          ["Fastmail", "bar@fastmail.fm", "mail.messagingengine.com"]
        ].each do |service, email, expected|
          context service do
            let(:new_email) { email }

            context "with nil" do
              let(:existing_server) { nil }

              it "sets a default server" do
                expect(account[:server]).to eq(expected)
              end
            end

            context "with an empty string" do
              let(:existing_server) { "" }

              it "sets a default server" do
                expect(account[:server]).to eq(expected)
              end
            end
          end
        end
      end

      context "the email is new" do
        it "modifies the email address" do
          expect(account[:username]).to eq(new_email)
        end

        include_examples "it flags the account as modified"
      end

      context "the email already exists" do
        let(:new_email) { other_email }

        it "indicates the error" do
          expect(subject).to have_received(:puts).with("There is already an account set up with that email address")
        end

        it "doesn't set the email" do
          expect(account[:username]).to eq(existing_email)
        end

        include_examples "it doesn't flag the account as modified"
      end
    end

    context "password" do
      let(:new_password) { "new_password" }

      before do
        allow(Imap::Backup::Configuration::Asker).to receive(:password).and_return(new_password)
        subject.run
        menu.choices["modify password"].call
      end

      context "if the user enters a password" do
        it "updates the password" do
          expect(account[:password]).to eq(new_password)
        end

        include_examples "it flags the account as modified"
      end

      context "if the user cancels" do
        let(:new_password) { nil }

        it "does nothing" do
          expect(account[:password]).to eq(existing_password)
        end

        include_examples "it doesn't flag the account as modified"
      end
    end

    context "server" do
      let(:server) { "server" }

      before do
        allow(highline).to receive(:ask).with("server: ").and_return(server)
      end

      before do
        subject.run
        menu.choices["modify server"].call
      end

      it "updates the server" do
        expect(account[:server]).to eq(server)
      end

      include_examples "it flags the account as modified"
    end

    context "backup_path" do
      let(:new_backup_path) { "/new/path" }

      before do
        @validator = nil
        allow(Imap::Backup::Configuration::Asker).to receive(:backup_path) do |path, validator|
          @validator = validator
          new_backup_path
        end
        subject.run
        menu.choices["modify backup path"].call
      end

      it "updates the path" do
        expect(account[:local_path]).to eq(new_backup_path)
      end

      it "validates that the path is not used by other backups" do
        expect(@validator.call(other_existing_path)).to be_falsey
      end

      include_examples "it flags the account as modified"
    end

    context "folders" do
      let(:chooser) { double(run: nil) }

      before do
        allow(Imap::Backup::Configuration::FolderChooser).to receive(:new).and_return(chooser)
        subject.run
        menu.choices["choose backup folders"].call
      end

      it "edits folders" do
        expect(chooser).to have_received(:run)
      end
    end

    context "connection test" do
      before do
        allow(Imap::Backup::Configuration::ConnectionTester).to receive(:test).and_return("All fine")
        allow(highline).to receive(:ask)
        subject.run
        menu.choices["test connection"].call
      end

      it "tests the connection" do
        expect(Imap::Backup::Configuration::ConnectionTester).to have_received(:test).with(account)
      end
    end

    context "deletion" do
      let(:confirmed) { true }

      before do
        allow(highline).to receive(:agree).and_return(confirmed)
        subject.run
        catch :done do
          menu.choices["delete"].call
        end
      end

      it "asks for confirmation" do
        expect(highline).to have_received(:agree)
      end

      context "when the user confirms deletion" do
        include_examples "it flags the account to be deleted"
      end

      context "without confirmation" do
        let(:confirmed) { false }

        include_examples "it doesn't flag the account to be deleted"
      end
    end
  end
end