require "ostruct"

describe Imap::Backup::Account::Connection do
  BACKUP_FOLDER = "backup_folder".freeze
  FOLDER_CONFIG = {name: BACKUP_FOLDER}.freeze
  FOLDER_NAME = "my_folder".freeze
  GMAIL_IMAP_SERVER = "imap.gmail.com".freeze
  LOCAL_PATH = "local_path".freeze
  LOCAL_UID = "local_uid".freeze
  PASSWORD = "secret".freeze
  ROOT_NAME = "foo".freeze
  SERVER = "imap.example.com".freeze
  USERNAME = "username@example.com".freeze

  subject { described_class.new(options) }

  let(:imap) do
    instance_double(Net::IMAP, authenticate: nil, login: nil, disconnect: nil)
  end
  let(:imap_folders) { [] }
  let(:options) do
    {
      username: USERNAME,
      password: PASSWORD,
      local_path: LOCAL_PATH,
      folders: config_folders,
      server: server
    }
  end
  let(:config_folders) { [FOLDER_CONFIG] }
  let(:root_info) do
    instance_double(Net::IMAP::MailboxList, name: ROOT_NAME)
  end
  let(:serializer) do
    instance_double(
      Imap::Backup::Serializer::Mbox,
      folder: serialized_folder,
      force_uid_validity: nil,
      apply_uid_validity: new_uid_validity,
      uids: [LOCAL_UID]
    )
  end
  let(:serialized_folder) { nil }
  let(:server) { SERVER }
  let(:new_uid_validity) { nil }

  before do
    allow(Net::IMAP).to receive(:new) { imap }
    allow(imap).to receive(:list).with("", "") { [root_info] }
    allow(imap).to receive(:list).with(ROOT_NAME, "*") { imap_folders }
    allow(Imap::Backup::Utils).to receive(:make_folder)
  end

  shared_examples "connects to IMAP" do
    it "logs in to the imap server" do
      expect(imap).to have_received(:login)
    end
  end

  describe "#initialize" do
    [
      [:username, USERNAME],
      [:password, PASSWORD],
      [:local_path, LOCAL_PATH],
      [:server, SERVER]
    ].each do |attr, expected|
      it "expects #{attr}" do
        expect(subject.public_send(attr)).to eq(expected)
      end
    end

    it "creates the path" do
      expect(Imap::Backup::Utils).to receive(:make_folder)

      subject.username
    end
  end

  describe "#imap" do
    let(:result) { subject.imap }

    it "returns the IMAP connection" do
      expect(result).to eq(imap)
    end

    it "uses the password" do
      result

      expect(imap).to have_received(:login).with(USERNAME, PASSWORD)
    end

    context "with the GMail IMAP server" do
      ACCESS_TOKEN = "access_token".freeze

      let(:server) { GMAIL_IMAP_SERVER }
      let(:refresh_token) { true }
      let(:result) { nil }
      let(:authenticator) do
        instance_double(
          Gmail::Authenticator,
          credentials: credentials
        )
      end
      let(:credentials) { OpenStruct.new(access_token: ACCESS_TOKEN) }

      before do
        allow(Gmail::Authenticator).
          to receive(:refresh_token?) { refresh_token }
        allow(Gmail::Authenticator).
          to receive(:new).
            with(email: USERNAME, token: PASSWORD) { authenticator }
      end

      context "when the password is our copy of a GMail refresh token" do
        it "uses the OAuth2 access_token to authenticate" do
          subject.imap

          expect(imap).to have_received(:authenticate).with(
            "XOAUTH2", USERNAME, ACCESS_TOKEN
          )
        end

        context "when the refresh token is invalid" do
          let(:credentials) { nil }

          it "raises" do
            expect { subject.imap }.to raise_error(String)
          end
        end
      end

      context "when the password is not our copy of a GMail refresh token" do
        let(:refresh_token) { false }

        it "uses the password" do
          subject.imap

          expect(imap).to have_received(:login).with(USERNAME, PASSWORD)
        end
      end
    end

    context "when the first login attempt fails" do
      before do
        outcomes = [-> { raise EOFError }, -> { true }]
        allow(imap).to receive(:login) { outcomes.shift.call }
      end

      it "retries" do
        subject.imap

        expect(imap).to have_received(:login).twice
      end
    end

    context "when run" do
      before { subject.imap }

      include_examples "connects to IMAP"
    end
  end

  describe "#folders" do
    let(:imap_folders) do
      [instance_double(Net::IMAP::MailboxList, name: BACKUP_FOLDER)]
    end

    it "returns the list of folders" do
      expect(subject.folders).to eq([BACKUP_FOLDER])
    end

    context "with non-ASCII folder names" do
      let(:imap_folders) do
        [instance_double(Net::IMAP::MailboxList, name: "Gel&APY-scht")]
      end

      it "converts them to UTF-8" do
        expect(subject.folders).to eq(["Gelöscht"])
      end
    end
  end

  describe "#status" do
    let(:folder) do
      instance_double(Imap::Backup::Account::Folder, uids: [remote_uid])
    end
    let(:remote_uid) { "remote_uid" }

    before do
      allow(Imap::Backup::Account::Folder).to receive(:new) { folder }
      allow(Imap::Backup::Serializer::Mbox).to receive(:new) { serializer }
    end

    it "returns the names of folders" do
      expect(subject.status[0][:name]).to eq(BACKUP_FOLDER)
    end

    it "returns local message uids" do
      expect(subject.status[0][:local]).to eq([LOCAL_UID])
    end

    it "retrieves the available uids" do
      expect(subject.status[0][:remote]).to eq([remote_uid])
    end
  end

  describe "#run_backup" do
    let(:folder) do
      instance_double(
        Imap::Backup::Account::Folder,
        name: "folder",
        exist?: exists,
        uid_validity: uid_validity
      )
    end
    let(:exists) { true }
    let(:uid_validity) { 123 }
    let(:downloader) { instance_double(Imap::Backup::Downloader, run: nil) }

    before do
      allow(Imap::Backup::Downloader).
        to receive(:new).with(folder, serializer) { downloader }
      allow(Imap::Backup::Account::Folder).to receive(:new).
        with(subject, BACKUP_FOLDER) { folder }
      allow(Imap::Backup::Serializer::Mbox).to receive(:new).
        with(LOCAL_PATH, BACKUP_FOLDER) { serializer }
    end

    context "with supplied config_folders" do
      it "runs the downloader" do
        expect(downloader).to receive(:run)

        subject.run_backup
      end

      context "when a folder does not exist" do
        let(:exists) { false }

        it "does not run the downloader" do
          expect(downloader).to_not receive(:run)

          subject.run_backup
        end
      end
    end

    context "without supplied config_folders" do
      let(:imap_folders) do
        [instance_double(Net::IMAP::MailboxList, name: ROOT_NAME)]
      end

      before do
        allow(Imap::Backup::Account::Folder).to receive(:new).
          with(subject, ROOT_NAME) { folder }
        allow(Imap::Backup::Serializer::Mbox).to receive(:new).
          with(LOCAL_PATH, ROOT_NAME) { serializer }
      end

      context "when supplied config_folders is nil" do
        let(:config_folders) { nil }

        it "runs the downloader for each folder" do
          expect(downloader).to receive(:run).exactly(:once)

          subject.run_backup
        end
      end

      context "when supplied config_folders is an empty list" do
        let(:config_folders) { [] }

        it "runs the downloader for each folder" do
          expect(downloader).to receive(:run).exactly(:once)

          subject.run_backup
        end
      end

      context "when the imap server doesn't return folders" do
        let(:config_folders) { nil }
        let(:imap_folders) { nil }

        it "fails" do
          expect do
            subject.run_backup
          end.to raise_error(RuntimeError, /Unable to get folder list/)
        end
      end
    end

    context "when the IMAP session expires" do
      before do
        data = OpenStruct.new(data: "Session expired")
        response = OpenStruct.new(data: data)
        outcomes = [
          -> { raise Net::IMAP::ByeResponseError, response },
          -> { nil }
        ]
        allow(downloader).to receive(:run) { outcomes.shift.call }
      end

      it "reconnects" do
        expect(downloader).to receive(:run).exactly(:twice)

        subject.run_backup
      end
    end

    context "when run" do
      before { subject.run_backup }

      include_examples "connects to IMAP"
    end
  end

  describe "#restore" do
    let(:folder) do
      instance_double(
        Imap::Backup::Account::Folder,
        create: nil,
        uids: uids,
        name: FOLDER_NAME,
        uid_validity: uid_validity
      )
    end
    let(:uids) { [99] }
    let(:uid_validity) { 123 }
    let(:serialized_folder) { "old name" }
    let(:uploader) do
      instance_double(Imap::Backup::Uploader, run: false)
    end
    let(:updated_uploader) do
      instance_double(Imap::Backup::Uploader, run: false)
    end
    let(:updated_folder) do
      instance_double(
        Imap::Backup::Account::Folder,
        create: nil,
        uid_validity: "new uid validity"
      )
    end
    let(:updated_serializer) do
      instance_double(
        Imap::Backup::Serializer::Mbox, force_uid_validity: nil
      )
    end

    before do
      allow(Imap::Backup::Account::Folder).to receive(:new).
        with(subject, FOLDER_NAME) { folder }
      allow(Imap::Backup::Serializer::Mbox).to receive(:new).
        with(anything, FOLDER_NAME) { serializer }
      allow(Imap::Backup::Account::Folder).to receive(:new).
        with(subject, "new name") { updated_folder }
      allow(Imap::Backup::Serializer::Mbox).to receive(:new).
        with(anything, "new name") { updated_serializer }
      allow(Imap::Backup::Uploader).to receive(:new).
        with(folder, serializer) { uploader }
      allow(Imap::Backup::Uploader).to receive(:new).
        with(updated_folder, updated_serializer) { updated_uploader }
      allow(Pathname).to receive(:glob).
        and_yield(Pathname.new(File.join(LOCAL_PATH, "#{FOLDER_NAME}.imap")))
    end

    it "sets local uid validity" do
      expect(serializer).to receive(:apply_uid_validity).with(uid_validity)

      subject.restore
    end

    context "when folders exist with contents" do
      context "when the local folder is renamed" do
        let(:new_uid_validity) { "new name" }

        it "creates the new folder" do
          expect(updated_folder).to receive(:create)

          subject.restore
        end

        it "sets the renamed folder's uid validity" do
          expect(updated_serializer).
            to receive(:force_uid_validity).with("new uid validity")

          subject.restore
        end

        it "creates the uploader with updated folder and serializer" do
          expect(updated_uploader).to receive(:run)

          subject.restore
        end
      end

      context "when the local folder is not renamed" do
        it "runs the uploader" do
          expect(uploader).to receive(:run)

          subject.restore
        end
      end
    end

    context "when folders don't exist or are empty" do
      let(:uids) { [] }

      it "creates the folder" do
        expect(folder).to receive(:create)

        subject.restore
      end

      it "forces local uid validity" do
        expect(serializer).to receive(:force_uid_validity).with(uid_validity)

        subject.restore
      end

      it "runs the uploader" do
        expect(uploader).to receive(:run)

        subject.restore
      end
    end
  end

  describe "#reconnect" do
    it "disconnects from the server" do
      expect(imap).to receive(:disconnect)

      subject.reconnect
    end

    it "causes reconnection on future access" do
      expect(Net::IMAP).to receive(:new)

      subject.reconnect
    end
  end

  describe "#disconnect" do
    it "disconnects from the server" do
      expect(imap).to receive(:disconnect)

      subject.disconnect
    end
  end
end
