RSpec.describe ROM::HTTP::Dataset do
  let(:klass) { ROM::HTTP::Dataset }
  let(:dataset) { klass.new(config, options) }
  let(:config) do
    {
      uri: uri,
      request_handler: request_handler,
      response_handler: response_handler
    }
  end
  let(:options) { {} }
  let(:uri) { 'http://localhost:3000' }
  let(:request_handler) { double(Proc) }
  let(:response_handler) { double(Proc) }

  it { expect(dataset).to be_kind_of(Enumerable) }

  describe 'defaults' do
    describe '#config' do
      subject { dataset.config }

      it { is_expected.to eq(config) }
    end

    describe '#options' do
      subject { dataset.options }

      context 'with options passed' do
        let(:options) do
          {
            request_method: :put,
            headers: {
              'Accept' => 'application/json'
            }
          }
        end

        it do
          is_expected.to eq(
            request_method: :put,
            path: '',
            projections: [],
            params: {},
            headers: {
              'Accept' => 'application/json'
            }
          )
        end
      end

      context 'with no options passed' do
        it do
          is_expected.to eq(
            request_method: :get,
            path: '',
            projections: [],
            params: {},
            headers: {}
          )
        end
      end
    end
  end

  describe '.default_request_handler' do
    before do
      module Test
        class Dataset < ROM::HTTP::Dataset; end
      end
    end

    context 'when no default_request_handler set' do
      it 'returns nil' do
        expect(klass.default_request_handler).to be nil
      end
    end

    context 'when default_request_handler set' do
      before do
        Test::Dataset.default_request_handler(request_handler)
      end

      it 'returns the default request handler' do
        expect(Test::Dataset.default_request_handler).to eq request_handler
      end
    end
  end

  describe '.default_response_handler' do
    before do
      module Test
        class Dataset < ROM::HTTP::Dataset; end
      end
    end

    context 'when no default_response_handler set' do
      it 'returns nil' do
        expect(klass.default_response_handler).to be nil
      end
    end

    context 'when default_response_handler set' do
      before do
        Test::Dataset.default_response_handler(response_handler)
      end

      it 'returns the default response handler' do
        expect(Test::Dataset.default_response_handler).to eq response_handler
      end
    end
  end

  describe '#response_transformer' do
    context 'with argument' do
      let(:transformer) { double('ResponseTransformer') }

      subject! { dataset.response_transformer(transformer) }

      it do
        expect(dataset.instance_variable_get(:@response_transformer))
          .to eq(transformer)
      end
      it { is_expected.to eq(transformer) }
    end

    context 'without argument' do
      context 'when transformer not set' do
        subject! { dataset.response_transformer }

        it { is_expected.to be_a(ROM::HTTP::Dataset::ResponseTransformers::Schemaless) }
      end

      context 'when transformer set' do
        let(:transformer) { double('ResponseTransformer') }

        before do
          dataset.response_transformer(transformer)
        end

        subject! { dataset.response_transformer }

        it { is_expected.to eq(transformer) }
      end
    end
  end

  describe '#uri' do
    context 'when no uri configured' do
      let(:config) { {} }

      it do
        expect { dataset.uri }.to raise_error(ROM::HTTP::Error)
      end
    end

    context 'when uri configured' do
      it { expect(dataset.uri).to eq(uri) }
    end
  end

  describe '#headers' do
    subject { dataset.headers }

    context 'with no headers configured' do
      context 'with no headers option' do
        it { is_expected.to eq({}) }
      end

      context 'with headers option' do
        let(:headers) { { 'Accept' => 'application/json' } }
        let(:options) { { headers: headers } }

        it { is_expected.to eq(headers) }
      end
    end

    context 'with headers configured' do
      context 'with no headers option' do
        let(:headers) { { 'Accept' => 'application/json' } }
        let(:config) do
          super().merge(headers: headers)
        end

        it { is_expected.to eq(headers) }
      end

      context 'with headers option' do
        let(:config_headers) { { 'Content-Type' => 'application/json' } }
        let(:config) do
          super().merge(headers: config_headers)
        end
        let(:option_headers) { { 'Accept' => 'application/json' } }
        let(:options) { { headers: option_headers } }

        it do
          is_expected.to eq(
            'Content-Type' => 'application/json',
            'Accept' => 'application/json'
          )
        end
      end
    end
  end

  describe '#name' do
    subject { dataset.name }

    context 'with no name configured' do
      it { is_expected.to eq('') }
    end

    context 'with name configured' do
      let(:name) { 'users' }
      let(:config) do
        super().merge(name: name)
      end

      it { is_expected.to eq(name) }
    end
  end

  describe '#path' do
    subject { dataset.path }

    context 'with no path option' do
      it { is_expected.to eq('') }
    end

    context 'with path option' do
      context 'when path is absolute' do
        let(:path) { '/users' }
        let(:options) { { path: path } }

        it 'removes the leading /' do
          is_expected.to eq('users')
        end
      end

      context 'when path is not absolute' do
        let(:path) { 'users' }
        let(:options) { { path: path } }

        it { is_expected.to eq(path) }
      end
    end
  end

  describe '#absolute_path' do
    subject { dataset.absolute_path }

    context 'with no path option' do
      it { is_expected.to eq('/') }
    end

    context 'with path option' do
      context 'when path is absolute' do
        let(:path) { '/users' }
        let(:options) { { path: path } }

        it { is_expected.to eq(path) }
      end

      context 'when path is not absolute' do
        let(:path) { 'users' }
        let(:options) { { path: path } }

        it { is_expected.to eq("/#{path}") }
      end
    end
  end

  describe '#request_method' do
    subject { dataset.request_method }

    context 'with no request_method option' do
      it { is_expected.to eq(:get) }
    end

    context 'with request_method option' do
      let(:request_method) { :put }
      let(:options) { { request_method: request_method } }

      it { is_expected.to eq(request_method) }
    end
  end

  describe '#params' do
    subject { dataset.params }

    context 'with no params option' do
      it { is_expected.to eq({}) }
    end

    context 'with params option' do
      let(:params) { { name: 'Jack' } }
      let(:options) { { params: params } }

      it { is_expected.to eq(params) }
    end
  end

  describe '#with_headers' do
    let(:headers) { { 'Accept' => 'application/json' } }
    let(:new_dataset) { dataset.with_headers(headers) }

    subject! { new_dataset }

    it { expect(new_dataset.config).to eq(config) }
    it do
      expect(new_dataset.options).to eq(
        request_method: :get,
        path: '',
        projections: [],
        params: {},
        headers: headers
      )
    end
    it { is_expected.to_not be(dataset) }
    it { is_expected.to be_a(ROM::HTTP::Dataset) }
  end

  describe '#add_header' do
    let(:header_key) { 'Accept' }
    let(:header_value) { 'application/json' }
    let(:new_dataset) { double(ROM::HTTP::Dataset) }

    before do
      allow(dataset).to receive(:with_headers).and_return(new_dataset)
    end

    subject! { dataset.add_header(header_key, header_value) }

    context 'with existing headers configured' do
      let(:config_headers) { { 'Content-Type' => 'application/json', 'Accept' => 'text/html' } }
      let(:config) { super().merge(headers: config_headers) }

      it do
        expect(dataset).to have_received(:with_headers).with(
          'Content-Type' => 'application/json',
          header_key => header_value
        )
      end
      it { is_expected.to eq(new_dataset) }
    end

    context 'without existing headers configured' do
      it do
        expect(dataset).to have_received(:with_headers).with(
          header_key => header_value
        )
      end
      it { is_expected.to eq(new_dataset) }
    end
  end

  describe '#with_options' do
    let(:name) { 'Jill' }
    let(:options) { { params: { name: name } } }
    let(:new_dataset) { dataset.with_options(options) }

    subject! { new_dataset }

    it { expect(new_dataset.config).to eq(config) }
    it do
      expect(new_dataset.options).to eq(
        request_method: :get,
        path: '',
        projections: [],
        params: {
          name: name
        },
        headers: {}
      )
    end
    it { is_expected.to_not be(dataset) }
    it { is_expected.to be_a(ROM::HTTP::Dataset) }
  end

  describe '#project' do
    let(:data) do
      [
        { id: 1, name: 'John', email: 'john@hotmail.com' },
        { id: 2, name: 'Jill', email: 'jill@hotmail.com' }
      ]
    end

    before do
      allow(request_handler).to receive(:call)
      allow(response_handler).to receive(:call).and_return(data)
    end

    subject! { dataset.project(*projections).to_a }

    context 'with projections' do
      context 'with a list of arguments' do
        let(:projections) { [:id, :name] }

        it 'applies the projections to the result set' do
          is_expected.to match_array([
            { id: 1, name: 'John' },
            { id: 2, name: 'Jill' }
          ])
        end
      end

      context 'with a single array argument' do
        let(:projections) { [[:id, :name]] }

        it 'applies the projections to the result set' do
          is_expected.to match_array([
            { id: 1, name: 'John' },
            { id: 2, name: 'Jill' }
          ])
        end
      end
    end

    context 'without projections' do
      let(:projections) { [] }

      it 'returns the original data' do
        is_expected.to match_array(data)
      end
    end
  end

  describe '#with_path' do
    let(:path) { '/users/tasks' }
    let(:new_dataset) { double(ROM::HTTP::Dataset) }

    before do
      allow(dataset).to receive(:with_options).and_return(new_dataset)
    end

    subject! { dataset.with_path(path) }

    it { expect(dataset).to have_received(:with_options).with(path: path) }
    it { is_expected.to eq(new_dataset) }
  end

  describe '#append_path' do
    let(:path) { 'tasks' }
    let(:new_dataset) { double(ROM::HTTP::Dataset) }

    before do
      allow(dataset).to receive(:with_options).and_return(new_dataset)
    end

    subject! { dataset.append_path(path) }

    context 'without existing path' do
      it { expect(dataset).to have_received(:with_options).with(path: '/tasks') }
      it { is_expected.to eq(new_dataset) }
    end

    context 'with existing path' do
      let(:options) { { path: '/users' } }

      it { expect(dataset).to have_received(:with_options).with(path: '/users/tasks') }
      it { is_expected.to eq(new_dataset) }
    end
  end

  describe '#with_request_method' do
    let(:request_method) { :put }
    let(:new_dataset) { double(ROM::HTTP::Dataset) }

    before do
      allow(dataset).to receive(:with_options).and_return(new_dataset)
    end

    subject! { dataset.with_request_method(request_method) }

    it { expect(dataset).to have_received(:with_options).with(request_method: request_method) }
    it { is_expected.to eq(new_dataset) }
  end

  describe '#with_params' do
    let(:name) { 'Jack' }
    let(:params) { { user: { name: name } } }
    let(:new_dataset) { double(ROM::HTTP::Dataset) }

    before do
      allow(dataset).to receive(:with_options).and_return(new_dataset)
    end

    subject! { dataset.with_params(params) }

    it { expect(dataset).to have_received(:with_options).with(params: params) }
    it { is_expected.to eq(new_dataset) }
  end

  describe '#each' do
    let(:response) { double(Array) }
    let(:block) { proc {} }
    let(:result) { double }

    before do
      allow(dataset).to receive(:response).and_return(response)
      allow(response).to receive(:each).and_yield.and_return(result)
    end

    context 'with no block given' do
      subject! { dataset.each }

      it { expect(dataset).to_not have_received(:response) }
      it { expect(response).to_not have_received(:each) }
      it { is_expected.to be_kind_of(Enumerable) }
    end

    context 'with block given' do
      subject! { dataset.each(&block) }

      it { expect(dataset).to have_received(:response).once }
      it { expect(response).to have_received(:each) }
      it { is_expected.to eq(result) }
    end
  end

  describe '#insert' do
    let(:name) { 'Jill' }
    let(:params) { { user: { name: name } } }
    let(:new_dataset) { double(ROM::HTTP::Dataset) }
    let(:response) { double }

    before do
      allow(dataset).to receive(:with_options).and_return(new_dataset)
      allow(new_dataset).to receive(:response).and_return(response)
    end

    subject! { dataset.insert(params) }

    it do
      expect(dataset).to have_received(:with_options).with(
        request_method: :post,
        params: params
      )
    end
    it { expect(new_dataset).to have_received(:response) }
    it { is_expected.to eq(response) }
  end

  describe '#update' do
    let(:name) { 'Jill' }
    let(:params) { { user: { name: name } } }
    let(:new_dataset) { double(ROM::HTTP::Dataset) }
    let(:response) { double }

    before do
      allow(dataset).to receive(:with_options).and_return(new_dataset)
      allow(new_dataset).to receive(:response).and_return(response)
    end

    subject! { dataset.update(params) }

    it do
      expect(dataset).to have_received(:with_options).with(
        request_method: :put,
        params: params
      )
    end
    it { expect(new_dataset).to have_received(:response) }
    it { is_expected.to eq(response) }
  end

  describe '#delete' do
    let(:new_dataset) { double(ROM::HTTP::Dataset) }
    let(:response) { double }

    before do
      allow(dataset).to receive(:with_options).and_return(new_dataset)
      allow(new_dataset).to receive(:response).and_return(response)
    end

    subject! { dataset.delete }

    it do
      expect(dataset).to have_received(:with_options).with(
        request_method: :delete
      )
    end
    it { expect(new_dataset).to have_received(:response) }
    it { is_expected.to eq(response) }
  end

  describe '#response' do
    let(:response) { double }
    let(:result) { double }

    context 'when request_handler and response_handler configured' do
      before do
        allow(request_handler).to receive(:call).and_return(response)
        allow(response_handler).to receive(:call).and_return(result)
      end

      subject! { dataset.response }

      it { expect(request_handler).to have_received(:call).with(dataset) }
      it { expect(response_handler).to have_received(:call).with(response, dataset) }
      it { is_expected.to eq(result) }
    end

    context 'when request_handler and response_handler configured' do
      let(:klass) { Test::Dataset }
      let(:config) { {} }

      before do
        module Test
          class Dataset < ROM::HTTP::Dataset; end
        end

        Test::Dataset.default_request_handler(request_handler)
        Test::Dataset.default_response_handler(response_handler)

        allow(request_handler).to receive(:call).and_return(response)
        allow(response_handler).to receive(:call).and_return(result)
      end

      subject! { dataset.response }

      it { expect(request_handler).to have_received(:call).with(dataset) }
      it { expect(response_handler).to have_received(:call).with(response, dataset) }
      it { is_expected.to eq(result) }
    end

    context 'when no request_handler configured and no default set' do
      let(:config) { { response_handler: response_handler } }

      it do
        expect { dataset.response }.to raise_error(ROM::HTTP::Error)
      end
    end

    context 'when no response_handler configured and no default set' do
      let(:config) { { request_handler: request_handler } }

      it do
        expect { dataset.response }.to raise_error(ROM::HTTP::Error)
      end
    end
  end
end
