# frozen_string_literal: true

RSpec.describe HTTP::Headers do
  subject(:headers) { described_class.new }

  it "is Enumerable" do
    expect(headers).to be_an Enumerable
  end

  describe "#set" do
    it "sets header value" do
      headers.set "Accept", "application/json"
      expect(headers["Accept"]).to eq "application/json"
    end

    it "allows retrieval via normalized header name" do
      headers.set :content_type, "application/json"
      expect(headers["Content-Type"]).to eq "application/json"
    end

    it "overwrites previous value" do
      headers.set :set_cookie, "hoo=ray"
      headers.set :set_cookie, "woo=hoo"
      expect(headers["Set-Cookie"]).to eq "woo=hoo"
    end

    it "allows set multiple values" do
      headers.set :set_cookie, "hoo=ray"
      headers.set :set_cookie, %w[hoo=ray woo=hoo]
      expect(headers["Set-Cookie"]).to eq %w[hoo=ray woo=hoo]
    end

    it "fails with empty header name" do
      expect { headers.set "", "foo bar" }.
        to raise_error HTTP::HeaderError
    end

    ["foo bar", "foo bar: ok\nfoo", "evil-header: evil-value\nfoo"].each do |name|
      it "fails with invalid header name (#{name.inspect})" do
        expect { headers.set name, "baz" }.
          to raise_error HTTP::HeaderError
      end
    end

    it "fails with invalid header value" do
      expect { headers.set "foo", "bar\nEvil-Header: evil-value" }.
        to raise_error HTTP::HeaderError
    end
  end

  describe "#[]=" do
    it "sets header value" do
      headers["Accept"] = "application/json"
      expect(headers["Accept"]).to eq "application/json"
    end

    it "allows retrieval via normalized header name" do
      headers[:content_type] = "application/json"
      expect(headers["Content-Type"]).to eq "application/json"
    end

    it "overwrites previous value" do
      headers[:set_cookie] = "hoo=ray"
      headers[:set_cookie] = "woo=hoo"
      expect(headers["Set-Cookie"]).to eq "woo=hoo"
    end

    it "allows set multiple values" do
      headers[:set_cookie] = "hoo=ray"
      headers[:set_cookie] = %w[hoo=ray woo=hoo]
      expect(headers["Set-Cookie"]).to eq %w[hoo=ray woo=hoo]
    end
  end

  describe "#delete" do
    before { headers.set "Content-Type", "application/json" }

    it "removes given header" do
      headers.delete "Content-Type"
      expect(headers["Content-Type"]).to be_nil
    end

    it "removes header that matches normalized version of specified name" do
      headers.delete :content_type
      expect(headers["Content-Type"]).to be_nil
    end

    it "fails with empty header name" do
      expect { headers.delete "" }.
        to raise_error HTTP::HeaderError
    end

    ["foo bar", "foo bar: ok\nfoo"].each do |name|
      it "fails with invalid header name (#{name.inspect})" do
        expect { headers.delete name }.
          to raise_error HTTP::HeaderError
      end
    end
  end

  describe "#add" do
    it "sets header value" do
      headers.add "Accept", "application/json"
      expect(headers["Accept"]).to eq "application/json"
    end

    it "allows retrieval via normalized header name" do
      headers.add :content_type, "application/json"
      expect(headers["Content-Type"]).to eq "application/json"
    end

    it "appends new value if header exists" do
      headers.add "Set-Cookie", "hoo=ray"
      headers.add :set_cookie, "woo=hoo"
      expect(headers["Set-Cookie"]).to eq %w[hoo=ray woo=hoo]
    end

    it "allows append multiple values" do
      headers.add :set_cookie, "hoo=ray"
      headers.add :set_cookie, %w[woo=hoo yup=pie]
      expect(headers["Set-Cookie"]).to eq %w[hoo=ray woo=hoo yup=pie]
    end

    it "fails with empty header name" do
      expect { headers.add("", "foobar") }.
        to raise_error HTTP::HeaderError
    end

    ["foo bar", "foo bar: ok\nfoo"].each do |name|
      it "fails with invalid header name (#{name.inspect})" do
        expect { headers.add name, "baz" }.
          to raise_error HTTP::HeaderError
      end
    end

    it "fails with invalid header value" do
      expect { headers.add "foo", "bar\nEvil-Header: evil-value" }.
        to raise_error HTTP::HeaderError
    end

    it "fails when header name is not a String or Symbol" do
      expect { headers.add 2, "foo" }.
        to raise_error HTTP::HeaderError
    end
  end

  describe "#get" do
    before { headers.set("Content-Type", "application/json") }

    it "returns array of associated values" do
      expect(headers.get("Content-Type")).to eq %w[application/json]
    end

    it "normalizes header name" do
      expect(headers.get(:content_type)).to eq %w[application/json]
    end

    context "when header does not exists" do
      it "returns empty array" do
        expect(headers.get(:accept)).to eq []
      end
    end

    it "fails with empty header name" do
      expect { headers.get("") }.
        to raise_error HTTP::HeaderError
    end

    ["foo bar", "foo bar: ok\nfoo"].each do |name|
      it "fails with invalid header name (#{name.inspect})" do
        expect { headers.get name }.
          to raise_error HTTP::HeaderError
      end
    end
  end

  describe "#[]" do
    context "when header does not exists" do
      it "returns nil" do
        expect(headers[:accept]).to be_nil
      end
    end

    context "when header has a single value" do
      before { headers.set "Content-Type", "application/json" }

      it "normalizes header name" do
        expect(headers[:content_type]).to_not be_nil
      end

      it "returns it returns a single value" do
        expect(headers[:content_type]).to eq "application/json"
      end
    end

    context "when header has a multiple values" do
      before do
        headers.add :set_cookie, "hoo=ray"
        headers.add :set_cookie, "woo=hoo"
      end

      it "normalizes header name" do
        expect(headers[:set_cookie]).to_not be_nil
      end

      it "returns array of associated values" do
        expect(headers[:set_cookie]).to eq %w[hoo=ray woo=hoo]
      end
    end
  end

  describe "#include?" do
    before do
      headers.add :content_type, "application/json"
      headers.add :set_cookie,   "hoo=ray"
      headers.add :set_cookie,   "woo=hoo"
    end

    it "tells whenever given headers is set or not" do
      expect(headers.include?("Content-Type")).to be true
      expect(headers.include?("Set-Cookie")).to be true
      expect(headers.include?("Accept")).to be false
    end

    it "normalizes given header name" do
      expect(headers.include?(:content_type)).to be true
      expect(headers.include?(:set_cookie)).to be true
      expect(headers.include?(:accept)).to be false
    end
  end

  describe "#to_h" do
    before do
      headers.add :content_type, "application/json"
      headers.add :set_cookie,   "hoo=ray"
      headers.add :set_cookie,   "woo=hoo"
    end

    it "returns a Hash" do
      expect(headers.to_h).to be_a ::Hash
    end

    it "returns Hash with normalized keys" do
      expect(headers.to_h.keys).to match_array %w[Content-Type Set-Cookie]
    end

    context "for a header with single value" do
      it "provides a value as is" do
        expect(headers.to_h["Content-Type"]).to eq "application/json"
      end
    end

    context "for a header with multiple values" do
      it "provides an array of values" do
        expect(headers.to_h["Set-Cookie"]).to eq %w[hoo=ray woo=hoo]
      end
    end
  end

  describe "#to_a" do
    before do
      headers.add :content_type, "application/json"
      headers.add :set_cookie,   "hoo=ray"
      headers.add :set_cookie,   "woo=hoo"
    end

    it "returns an Array" do
      expect(headers.to_a).to be_a Array
    end

    it "returns Array of key/value pairs with normalized keys" do
      expect(headers.to_a).to eq [
        %w[Content-Type application/json],
        %w[Set-Cookie hoo=ray],
        %w[Set-Cookie woo=hoo]
      ]
    end
  end

  describe "#inspect" do
    before  { headers.set :set_cookie, %w[hoo=ray woo=hoo] }
    subject { headers.inspect }

    it { is_expected.to eq '#<HTTP::Headers {"Set-Cookie"=>["hoo=ray", "woo=hoo"]}>' }
  end

  describe "#keys" do
    before do
      headers.add :content_type, "application/json"
      headers.add :set_cookie,   "hoo=ray"
      headers.add :set_cookie,   "woo=hoo"
    end

    it "returns uniq keys only" do
      expect(headers.keys.size).to eq 2
    end

    it "normalizes keys" do
      expect(headers.keys).to include("Content-Type", "Set-Cookie")
    end
  end

  describe "#each" do
    before do
      headers.add :set_cookie,   "hoo=ray"
      headers.add :content_type, "application/json"
      headers.add :set_cookie,   "woo=hoo"
    end

    it "yields each key/value pair separatedly" do
      expect { |b| headers.each(&b) }.to yield_control.exactly(3).times
    end

    it "yields headers in the same order they were added" do
      expect { |b| headers.each(&b) }.to yield_successive_args(
        %w[Set-Cookie hoo=ray],
        %w[Content-Type application/json],
        %w[Set-Cookie woo=hoo]
      )
    end

    it "yields header keys specified as symbols in normalized form" do
      keys = headers.each.map(&:first)
      expect(keys).to eq(%w[Set-Cookie Content-Type Set-Cookie])
    end

    it "yields headers specified as strings without conversion" do
      headers.add "X_kEy", "value"
      keys = headers.each.map(&:first)
      expect(keys).to eq(%w[Set-Cookie Content-Type Set-Cookie X_kEy])
    end

    it "returns self instance if block given" do
      expect(headers.each { |*| }).to be headers # rubocop:disable Lint/EmptyBlock
    end

    it "returns Enumerator if no block given" do
      expect(headers.each).to be_a Enumerator
    end
  end

  describe ".empty?" do
    subject { headers.empty? }

    context "initially" do
      it { is_expected.to be true }
    end

    context "when header exists" do
      before { headers.add :accept, "text/plain" }
      it { is_expected.to be false }
    end

    context "when last header was removed" do
      before do
        headers.add :accept, "text/plain"
        headers.delete :accept
      end

      it { is_expected.to be true }
    end
  end

  describe "#hash" do
    let(:left)  { described_class.new }
    let(:right) { described_class.new }

    it "equals if two headers equals" do
      left.add :accept, "text/plain"
      right.add :accept, "text/plain"

      expect(left.hash).to eq right.hash
    end
  end

  describe "#==" do
    let(:left)  { described_class.new }
    let(:right) { described_class.new }

    it "compares header keys and values" do
      left.add :accept, "text/plain"
      right.add :accept, "text/plain"

      expect(left).to eq right
    end

    it "allows comparison with Array of key/value pairs" do
      left.add :accept, "text/plain"
      expect(left).to eq [%w[Accept text/plain]]
    end

    it "sensitive to headers order" do
      left.add :accept, "text/plain"
      left.add :cookie, "woo=hoo"
      right.add :cookie, "woo=hoo"
      right.add :accept, "text/plain"

      expect(left).to_not eq right
    end

    it "sensitive to header values order" do
      left.add :cookie, "hoo=ray"
      left.add :cookie, "woo=hoo"
      right.add :cookie, "woo=hoo"
      right.add :cookie, "hoo=ray"

      expect(left).to_not eq right
    end
  end

  describe "#dup" do
    before { headers.set :content_type, "application/json" }

    subject(:dupped) { headers.dup }

    it { is_expected.to be_a described_class }
    it { is_expected.not_to be headers }

    it "has headers copied" do
      expect(dupped[:content_type]).to eq "application/json"
    end

    context "modifying a copy" do
      before { dupped.set :content_type, "text/plain" }

      it "modifies dupped copy" do
        expect(dupped[:content_type]).to eq "text/plain"
      end

      it "does not affects original headers" do
        expect(headers[:content_type]).to eq "application/json"
      end
    end
  end

  describe "#merge!" do
    before do
      headers.set :host, "example.com"
      headers.set :accept, "application/json"
      headers.merge! :accept => "plain/text", :cookie => %w[hoo=ray woo=hoo]
    end

    it "leaves headers not presented in other as is" do
      expect(headers[:host]).to eq "example.com"
    end

    it "overwrites existing values" do
      expect(headers[:accept]).to eq "plain/text"
    end

    it "appends other headers, not presented in base" do
      expect(headers[:cookie]).to eq %w[hoo=ray woo=hoo]
    end
  end

  describe "#merge" do
    before do
      headers.set :host, "example.com"
      headers.set :accept, "application/json"
    end

    subject(:merged) do
      headers.merge :accept => "plain/text", :cookie => %w[hoo=ray woo=hoo]
    end

    it { is_expected.to be_a described_class }
    it { is_expected.not_to be headers }

    it "does not affects original headers" do
      expect(merged.to_h).to_not eq headers.to_h
    end

    it "leaves headers not presented in other as is" do
      expect(merged[:host]).to eq "example.com"
    end

    it "overwrites existing values" do
      expect(merged[:accept]).to eq "plain/text"
    end

    it "appends other headers, not presented in base" do
      expect(merged[:cookie]).to eq %w[hoo=ray woo=hoo]
    end
  end

  describe ".coerce" do
    let(:dummyClass) { Class.new { def respond_to?(*); end } }

    it "accepts any object that respond to #to_hash" do
      hashie = double :to_hash => {"accept" => "json"}
      expect(described_class.coerce(hashie)["accept"]).to eq "json"
    end

    it "accepts any object that respond to #to_h" do
      hashie = double :to_h => {"accept" => "json"}
      expect(described_class.coerce(hashie)["accept"]).to eq "json"
    end

    it "accepts any object that respond to #to_a" do
      hashie = double :to_a => [%w[accept json]]
      expect(described_class.coerce(hashie)["accept"]).to eq "json"
    end

    it "fails if given object cannot be coerced" do
      expect { described_class.coerce dummyClass.new }.to raise_error HTTP::Error
    end

    context "with duplicate header keys (mixed case)" do
      let(:headers) { {"Set-Cookie" => "hoo=ray", "set_cookie" => "woo=hoo", :set_cookie => "ta=da"} }

      it "adds all headers" do
        expect(described_class.coerce(headers).to_a).
          to match_array(
            [
              %w[Set-Cookie hoo=ray],
              %w[set_cookie woo=hoo],
              %w[Set-Cookie ta=da]
            ]
          )
      end
    end

    it "is aliased as .[]" do
      expect(described_class.method(:coerce)).to eq described_class.method(:[])
    end
  end
end
