# encoding=utf-8
# frozen_string_literal: false

require "spec_helper"

describe WebSocket::Driver::Draft76 do
  include EncodingHelper

  let :body do
    encode [0x91, 0x25, 0x3e, 0xd3, 0xa9, 0xe7, 0x6a, 0x88]
  end

  let :response do
    string = "\xB4\x9Cn@S\x04\x04&\xE5\e\xBFl\xB7\x9F\x1D\xF9"
    string.force_encoding(Encoding::BINARY) if string.respond_to?(:force_encoding)
    string
  end

  let :env do
    {
      "REQUEST_METHOD"          => "GET",
      "HTTP_CONNECTION"         => "Upgrade",
      "HTTP_UPGRADE"            => "WebSocket",
      "HTTP_ORIGIN"             => "http://www.example.com",
      "HTTP_SEC_WEBSOCKET_KEY1" => "1   38 wZ3f9 23O0 3l 0r",
      "HTTP_SEC_WEBSOCKET_KEY2" => "27   0E 6 2  1665:< ;U 1H",
      "rack.input"              => StringIO.new(body)
    }
  end

  let :socket do
    socket = double(WebSocket)
    allow(socket).to receive(:env).and_return(env)
    allow(socket).to receive(:url).and_return("ws://www.example.com/socket")
    allow(socket).to receive(:write) { |message| @bytes = bytes(message) }
    socket
  end

  let :driver do
    driver = WebSocket::Driver::Draft76.new(socket)
    driver.on(:open)    { |e| @open = true }
    driver.on(:message) { |e| @message += e.data }
    driver.on(:error)   { |e| @error = e }
    driver.on(:close)   { |e| @close = true }
    driver
  end

  before do
    @open = @close = false
    @message = ""
  end

  describe "in the :connecting state" do
    it "starts in the connecting state" do
      expect(driver.state).to eq :connecting
    end

    describe :start do
      it "writes the handshake response to the socket" do
        expect(socket).to receive(:write).with(
            "HTTP/1.1 101 WebSocket Protocol Handshake\r\n" +
            "Upgrade: WebSocket\r\n" +
            "Connection: Upgrade\r\n" +
            "Sec-WebSocket-Origin: http://www.example.com\r\n" +
            "Sec-WebSocket-Location: ws://www.example.com/socket\r\n" +
            "\r\n")
        expect(socket).to receive(:write).with(response)
        driver.start
      end

      it "returns true" do
        expect(driver.start).to eq true
      end

      it "triggers the onopen event" do
        driver.start
        expect(@open).to eq true
      end

      it "changes the state to :open" do
        driver.start
        expect(driver.state).to eq :open
      end

      it "sets the protocol version" do
        driver.start
        expect(driver.version).to eq "hixie-76"
      end

      describe "with an invalid key header" do
        before do
          env["HTTP_SEC_WEBSOCKET_KEY1"] = "2 L785 8o% s9Sy9@V. 4<1P5"
        end

        it "writes a handshake error response" do
          expect(socket).to receive(:write).with(
              "HTTP/1.1 400 Bad Request\r\n" +
              "Content-Type: text/plain\r\n" +
              "Content-Length: 45\r\n" +
              "\r\n" +
              "Client sent invalid Sec-WebSocket-Key headers")
          driver.start
        end

        it "does not trigger the onopen event" do
          driver.start
          expect(@open).to eq false
        end

        it "triggers the onerror event" do
          driver.start
          expect(@error.message).to eq "Client sent invalid Sec-WebSocket-Key headers"
        end

        it "triggers the onclose event" do
          driver.start
          expect(@close).to eq true
        end

        it "changes the state to closed" do
          driver.start
          expect(driver.state).to eq :closed
        end
      end
    end

    describe :frame do
      it "does not write to the socket" do
        expect(socket).not_to receive(:write)
        driver.frame("Hello, world")
      end

      it "returns true" do
        expect(driver.frame("whatever")).to eq true
      end

      it "queues the frames until the handshake has been sent" do
        expect(socket).to receive(:write).with(
            "HTTP/1.1 101 WebSocket Protocol Handshake\r\n" +
            "Upgrade: WebSocket\r\n" +
            "Connection: Upgrade\r\n" +
            "Sec-WebSocket-Origin: http://www.example.com\r\n" +
            "Sec-WebSocket-Location: ws://www.example.com/socket\r\n" +
            "\r\n")
        expect(socket).to receive(:write).with(response)
        expect(socket).to receive(:write).with(encode "\x00Hi\xFF".bytes)

        driver.frame("Hi")
        driver.start

        expect(@bytes).to eq [0x00, 72, 105, 0xff]
      end
    end

    describe "with no request body" do
      before { env.delete("rack.input") }

      describe :start do
        it "writes the handshake response with no body" do
          expect(socket).to receive(:write).with(
              "HTTP/1.1 101 WebSocket Protocol Handshake\r\n" +
              "Upgrade: WebSocket\r\n" +
              "Connection: Upgrade\r\n" +
              "Sec-WebSocket-Origin: http://www.example.com\r\n" +
              "Sec-WebSocket-Location: ws://www.example.com/socket\r\n" +
              "\r\n")
          driver.start
        end

        it "does not trigger the onopen event" do
          driver.start
          expect(@open).to eq false
        end

        it "leaves the protocol in the :connecting state" do
          driver.start
          expect(driver.state).to eq :connecting
        end

        describe "when the request body is received" do
          before { driver.start }

          it "sends the response body" do
            expect(socket).to receive(:write).with(response)
            driver.parse(body)
          end

          it "triggers the onopen event" do
            driver.parse(body)
            expect(@open).to eq true
          end

          it "changes the state to :open" do
            driver.parse(body)
            expect(driver.state).to eq :open
          end

          it "sends any frames queued before the handshake was complete" do
            expect(socket).to receive(:write).with(response)
            expect(socket).to receive(:write).with(encode "\x00hello\xFF".bytes)
            driver.frame("hello")
            driver.parse(body)
            expect(@bytes).to eq [0, 104, 101, 108, 108, 111, 255]
          end
        end
      end
    end
  end

  it_should_behave_like "draft-75 protocol"

  describe "in the :open state" do
    before { driver.start }

    describe :parse do
      it "closes the socket if a close frame is received" do
        driver.parse [0xff, 0x00].pack("C*")
        expect(@close).to eq true
        expect(driver.state).to eq :closed
      end
    end

    describe :close do
      it "writes a close message to the socket" do
        driver.close
        expect(@bytes).to eq [0xff, 0x00]
      end
    end
  end

  describe "frozen rack.input.read" do
    let :frozen_env do
      # Make up a rack.input that somehow returns a frozen string on input.read
      # We're seeing this error occasionally when using ActionCable in Rails 5.2
      env.merge("rack.input" => Struct.new(:read).new(''.freeze))
    end

    it 'edge case where rack.input.read returns a frozen string' do
      frozen_socket = socket
      allow(frozen_socket).to receive(:env).and_return(frozen_env)

      expect {
        WebSocket::Driver::Draft76.new(frozen_socket)
      }.to_not raise_error
    end
  end
end
