# frozen_string_literal: true

module ImapGuard
  describe Guard do
    before do
      $stdout = StringIO.new # mute stdout - comment to debug
    end

    after do
      $stdout = STDOUT
    end

    let(:settings) do
      {
        host: "localhost",
        port: 993,
        username: "bob",
        password: "PASS",
      }
    end

    let(:imap) do
      double("Net::IMAP", search: [7, 28], expunge: nil, select: nil, list: [])
    end

    def guard_instance(custom_settings = {})
      guard = Guard.new(settings.merge(custom_settings))
      guard.instance_variable_set(:@imap, imap)
      allow(guard).to receive(:fetch_mail)
      guard
    end

    describe "#select" do
      context "with settings.read_only = true" do
        let(:guard) { guard_instance(read_only: true) }

        it "opens the mailbox in read-only" do
          expect(imap).to receive(:examine)
          guard.select nil
        end
      end

      context "with settings.read_only = false" do
        let(:guard) { guard_instance(read_only: false) }

        it "opens the mailbox in read-write" do
          expect(imap).to receive(:select)
          guard.select nil
        end
      end
    end

    describe "#mailbox" do
      it "returns nil when no mailbox has been selected" do
        expect(guard_instance.mailbox).to be_nil
      end

      it "returns the currently selected mailbox" do
        guard = guard_instance

        guard.select "Sent"
        expect(guard.mailbox).to eq "Sent"
      end
    end

    describe "#list" do
      it "returns the list of mailboxes" do
        expect(imap).to receive(:list)
        expect(guard_instance.list).to eq []
      end
    end

    describe "#search" do
      before do
        expect(imap).to receive(:search) do |arg|
          [13, 37] if [["ALL"], "ALL"].include? arg
        end
      end

      it "accepts arrays" do
        expect do
          guard_instance.send(:search, ["ALL"])
        end.to_not raise_error
      end

      it "accepts strings" do
        expect do
          guard_instance.send(:search, "ALL")
        end.to_not raise_error
      end

      it "returns results" do
        messages = guard_instance.send(:search, "ALL")
        expect(messages).to eq [13, 37]
      end
    end

    describe "#process" do
      let(:guard) { guard_instance(verbose: true) }
      let(:opeartion) { ->(id) {} }

      context "without a filter block" do
        it "does perform the operation" do
          expect(opeartion).to receive(:call).with(7)
          expect(opeartion).to receive(:call).with(28)

          guard.send(:process, "ALL", opeartion)
        end

        it "does not execute the filter block" do
          expect(guard).not_to receive(:fetch_mail)

          guard.send(:process, "ALL", opeartion)
        end

        context "with a debug proc" do
          it "calls the proc" do
            block = ->(mail) {}
            guard.debug = block
            expect(block).to receive(:call).twice

            guard.send(:process, "ALL", opeartion)
          end
        end
      end

      context "with a filter block" do
        it "executes the filter block" do
          expect(guard).to receive(:fetch_mail).twice

          guard.send(:process, "ALL", opeartion) { nil }
        end

        context "returning false" do
          it "does not perform the operation" do
            expect(opeartion).not_to receive(:call)

            guard.send(:process, "ALL", opeartion) { false }
          end
        end

        context "returning true" do
          it "does perform the operation" do
            expect(opeartion).to receive(:call).twice

            guard.send(:process, "ALL", opeartion) { true }
          end
        end
      end
    end

    describe "#move" do
      it "copies emails before adding the :Deleted flag" do
        expect(imap).to receive(:search)
        expect(imap).to receive(:copy).with(7, "destination").ordered
        expect(imap).to receive(:store).with(7, "+FLAGS", [:Deleted]).ordered
        expect(imap).to receive(:copy).with(28, "destination").ordered
        expect(imap).to receive(:store).with(28, "+FLAGS", [:Deleted]).ordered

        guard_instance.move "ALL", "destination"
      end
    end

    describe "#delete" do
      it "adds the :Deleted flag" do
        expect(imap).to receive(:search)
        expect(imap).to receive(:store).with(7, "+FLAGS", [:Deleted])
        expect(imap).to receive(:store).with(28, "+FLAGS", [:Deleted])

        guard_instance.delete "ALL"
      end
    end

    describe "#each" do
      it "iterates over messages without errors" do
        guard_instance.each "ALL" do |message_id|
          # noop
        end
      end
    end

    describe "#expunge" do
      it "expunges the folder" do
        expect(imap).to receive(:expunge)
        guard_instance.expunge
      end
    end

    describe "#close" do
      it "closes the IMAP connection" do
        expect(imap).to receive(:close)
        guard_instance.close
      end
    end

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

    describe "#verbose" do
      context "with settings.verbose = true" do
        let(:guard) { guard_instance(verbose: true) }

        it "does output to $stdout" do
          expect($stdout).to receive(:write).with("ham")
          guard.send(:verbose).print "ham"
        end
      end

      context "with settings.verbose = false" do
        let(:guard) { guard_instance(verbose: false) }

        it "does not output to $stdout" do
          expect($stdout).not_to receive(:write)
          guard.send(:verbose).print "ham"
        end
      end
    end

    describe "#settings=" do
      it "freezes the settings" do
        guard = Guard.new(settings)

        exception = (RUBY_VERSION >= "2.1.0" ? RuntimeError : TypeError)

        expect do
          guard.settings.host = "example.net"
        end.to raise_error(exception, /frozen/)
      end

      it "raises ArgumentError if any required key is missing" do
        expect do
          Guard.new({})
        end.to raise_error ArgumentError, /missing/i
      end

      it "raises ArgumentError if any key is unknown" do
        expect do
          Guard.new(settings.merge(coffee: true))
        end.to raise_error ArgumentError, /unknown/i
      end
    end
  end
end
