require "spec_helper"

describe JsRoutes, "compatibility with Rails"  do

  let(:generated_js) do
    JsRoutes.generate({module_type: nil, namespace: 'Routes'})
  end
  before(:each) do
    evaljs(generated_js)
  end

  it "should generate collection routing" do
    expect(evaljs("Routes.inboxes_path()")).to eq(test_routes.inboxes_path())
  end

  it "should generate member routing" do
    expect(evaljs("Routes.inbox_path(1)")).to eq(test_routes.inbox_path(1))
  end

  it "should raise error if required argument is not passed", :aggregate_failures do
    expect { evaljs("Routes.thing_path()") }
      .to raise_error(/Route missing required keys: id/)
    expect { evaljs("Routes.search_path()") }
      .to raise_error(/Route missing required keys: q/)
    expect { evaljs("Routes.book_path()") }
      .to raise_error(/Route missing required keys: section, title/)
    expect { evaljs("Routes.book_title_path()") }
      .to raise_error(/Route missing required keys: title/)

    expect( evaljs("try {Routes.thing_path()} catch (e) { e.name }") ).to eq('ParametersMissing')
    expect( evaljs("try {Routes.thing_path()} catch (e) { e.keys }") ).to eq(['id'])
  end

  it "should produce error stacktraces including function names" do
     stacktrace = evaljs("
        (function(){
          try {
            Routes.thing_path()
          } catch(e) {
            return e.stack;
          }
        })()
      ")
    expect(stacktrace).to include "thing_path"
  end

  it "should support 0 as a member parameter" do
    expect(evaljs("Routes.inbox_path(0)")).to eq(test_routes.inbox_path(0))
  end

  it "should generate nested routing with one parameter" do
    expect(evaljs("Routes.inbox_messages_path(1)")).to eq(test_routes.inbox_messages_path(1))
  end

  it "should generate nested routing" do
    expect(evaljs("Routes.inbox_message_path(1,2)")).to eq(test_routes.inbox_message_path(1, 2))
  end

  it "should generate routing with format" do
    expect(evaljs("Routes.inbox_path(1, {format: 'json'})")).to eq(test_routes.inbox_path(1, :format => "json"))
  end

  it "should support routes with reserved javascript words as parameters" do
    expect(evaljs("Routes.object_path(1, 2)")).to eq(test_routes.object_path(1,2))
  end

  it "should support routes with trailing_slash" do
    expect(evaljs("Routes.inbox_path(1, {trailing_slash: true})")).to eq(test_routes.inbox_path(1, trailing_slash: true))
  end

  it "should support url anchor given as parameter" do
    expect(evaljs("Routes.inbox_path(1, {anchor: 'hello'})")).to eq(test_routes.inbox_path(1, :anchor => "hello"))
  end

  it "should support url anchor and get parameters" do
    expect(evaljs("Routes.inbox_path(1, {expanded: true, anchor: 'hello'})")).to eq(test_routes.inbox_path(1, :expanded => true, :anchor => "hello"))
  end

  it "should support required parameters given as options hash" do
    expect(evaljs("Routes.search_path({q: 'hello'})")).to eq(test_routes.search_path(:q => 'hello'))
  end

  it "should use irregular ActiveSupport pluralizations" do
    expect(evaljs("Routes.budgies_path()")).to eq(test_routes.budgies_path)
    expect(evaljs("Routes.budgie_path(1)")).to eq(test_routes.budgie_path(1))
    expect(evaljs("Routes.budgy_path")).to eq(nil)
    expect(evaljs("Routes.budgie_descendents_path(1)")).to eq(test_routes.budgie_descendents_path(1))
  end

  it "should support route with parameters containing symbols that need URI-encoding", :aggregate_failures do
    expect(evaljs("Routes.inbox_path('#hello')")).to eq(test_routes.inbox_path('#hello'))
    expect(evaljs("Routes.inbox_path('some param')")).to eq(test_routes.inbox_path('some param'))
    expect(evaljs("Routes.inbox_path('some param with more & more encode symbols')")).to eq(test_routes.inbox_path('some param with more & more encode symbols'))
  end
  it "should support route with parameters containing symbols not need URI-encoding", :aggregate_failures do
    expect(evaljs("Routes.inbox_path(':some_id')")).to eq(test_routes.inbox_path(':some_id'))
    expect(evaljs("Routes.inbox_path('.+')")).to eq(test_routes.inbox_path('.+'))
  end

  describe "when route has defaults" do
    it "should support route default format" do
      expect(evaljs("Routes.api_purchases_path()")).to eq(test_routes.api_purchases_path)
    end

    it 'should support route default subdomain' do
      expect(evaljs("Routes.backend_root_path()")).to eq(test_routes.backend_root_path)
    end

    it "should support default format override" do
      expect(evaljs("Routes.api_purchases_path({format: 'xml'})")).to eq(test_routes.api_purchases_path(format: 'xml'))
    end

    it "should support default format override by passing it in args" do
      expect(evaljs("Routes.api_purchases_path('xml')")).to eq(test_routes.api_purchases_path('xml'))
    end

    it "doesn't apply defaults to path" do
      expect(evaljs("Routes.with_defaults_path()")).to eq(test_routes.with_defaults_path)
      expect(evaljs("Routes.with_defaults_path({format: 'json'})")).to eq(test_routes.with_defaults_path(format: 'json'))
    end
  end

  context "with rails engines" do
    it "should support simple route" do
      expect(evaljs("Routes.blog_app_posts_path()")).to eq(blog_routes.posts_path())
    end

    it "should support root route" do
      expect(evaljs("Routes.blog_app_path()")).to eq(test_routes.blog_app_path())
    end

    it "should support route with parameters" do
      expect(evaljs("Routes.blog_app_post_path(1)")).to eq(blog_routes.post_path(1))
    end
    it "should support root path" do
      expect(evaljs("Routes.blog_app_root_path()")).to eq(blog_routes.root_path)
    end
    it "should support single route mapping" do
      expect(evaljs("Routes.support_path({page: 3})")).to eq(test_routes.support_path(:page => 3))
    end

    it 'works' do
      expect(evaljs("Routes.planner_manage_path({locale: 'ua'})")).to eq(planner_routes.manage_path(locale: 'ua'))
      expect(evaljs("Routes.planner_manage_path()")).to eq(planner_routes.manage_path)
    end
  end

  it "shouldn't require the format" do
    expect(evaljs("Routes.json_only_path({format: 'json'})")).to eq(test_routes.json_only_path(:format => 'json'))
  end

  it "should serialize object with empty string value" do
    expect(evaljs("Routes.inboxes_path({a: '', b: 1})")).to eq(test_routes.inboxes_path(:a => '', :b => 1))
  end

  it "should support utf-8 route" do
    expect(evaljs("Routes.hello_path()")).to eq(test_routes.hello_path)
  end

  it "should support root_path" do
    expect(evaljs("Routes.root_path()")).to eq(test_routes.root_path)
  end

  describe "get parameters" do
    it "should support simple get parameters" do
      expect(evaljs("Routes.inbox_path(1, {format: 'json', lang: 'ua', q: 'hello'})")).to eq(test_routes.inbox_path(1, :lang => "ua", :q => "hello", :format => "json"))
    end

    it "should support array get parameters" do
      expect(evaljs("Routes.inbox_path(1, {hello: ['world', 'mars']})")).to eq(test_routes.inbox_path(1, :hello => [:world, :mars]))
    end

    context "object without prototype" do
      before(:each) do
        evaljs("let params = Object.create(null); params.q = 'hello';")
        evaljs("let inbox = Object.create(null); inbox.to_param = 1;")
      end

      it "should still work correctly" do
        expect(evaljs("Routes.inbox_path(inbox, params)")).to eq(
          test_routes.inbox_path(1, q: "hello")
        )
      end
    end

    it "should support nested get parameters" do
      expect(evaljs("Routes.inbox_path(1, {format: 'json', env: 'test', search: { category_ids: [2,5], q: 'hello'}})")).to eq(
        test_routes.inbox_path(1, :env => 'test', :search => {:category_ids => [2,5], :q => "hello"}, :format => "json")
      )
    end

    it "should support null and undefined parameters" do
      expect(evaljs("Routes.inboxes_path({uri: null, key: undefined})")).to eq(test_routes.inboxes_path(:uri => nil, :key => nil))
    end

    it "should escape get parameters" do
      expect(evaljs("Routes.inboxes_path({uri: 'http://example.com'})")).to eq(test_routes.inboxes_path(:uri => 'http://example.com'))
    end

    it "should support nested object null parameters" do
      expect(evaljs("Routes.inboxes_path({hello: {world: null}})")).to eq(test_routes.inboxes_path(:hello => {:world => nil}))
    end
  end


  context "routes globbing" do
    it "should be supported as parameters" do
      expect(evaljs("Routes.book_path('thrillers', 1)")).to eq(test_routes.book_path('thrillers', 1))
    end

    it "should support routes globbing as array" do
      expect(evaljs("Routes.book_path(['thrillers'], 1)")).to eq(test_routes.book_path(['thrillers'], 1))
    end

    it "should support routes globbing as array" do
      expect(evaljs("Routes.book_path([1, 2, 3], 1)")).to eq(test_routes.book_path([1, 2, 3], 1))
    end

    it "should support routes globbing with slash" do
      expect(evaljs("Routes.book_path('a_test/b_test/c_test', 1)")).to eq(test_routes.book_path('a_test/b_test/c_test', 1))
    end

    it "should support routes globbing as hash" do
      expect(evaljs("Routes.book_path('a%b', 1)")).to eq(test_routes.book_path('a%b', 1))
    end

    it "should support routes globbing as array with optional params" do
      expect(evaljs("Routes.book_path([1, 2, 3, 5], 1, {c: '1'})")).to eq(test_routes.book_path([1, 2, 3, 5], 1, { :c => "1" }))
    end

    it "should support routes globbing in book_title route as array" do
      expect(evaljs("Routes.book_title_path('john', ['thrillers', 'comedian'])")).to eq(test_routes.book_title_path('john', ['thrillers', 'comedian']))
    end

    it "should support routes globbing in book_title route as array with optional params" do
      expect(evaljs("Routes.book_title_path('john', ['thrillers', 'comedian'], {some_key: 'some_value'})")).to eq(test_routes.book_title_path('john', ['thrillers', 'comedian'], {:some_key => 'some_value'}))
    end
  end

  context "using optional path fragments" do
    context "including not optional parts" do
      it "should include everything that is not optional" do
        expect(evaljs("Routes.foo_path()")).to eq(test_routes.foo_path)
      end
    end

    context "but not including them" do
      it "should not include the optional parts" do
        expect(evaljs("Routes.things_path()")).to eq(test_routes.things_path)
        expect(evaljs("Routes.things_path({ q: 'hello' })")).to eq(test_routes.things_path(q: 'hello'))
      end

      it "treats false as absent optional part" do
        pending("https://github.com/rails/rails/issues/42280")
        expect(evaljs("Routes.things_path(false)")).to eq(test_routes.things_path(false))
      end

      it "treats false as absent optional part when default is specified" do
        expect(evaljs("Routes.campaigns_path(false)")).to eq(test_routes.campaigns_path(false))
      end

      it "should not require the optional parts as arguments" do
        expect(evaljs("Routes.thing_path(null, 5)")).to eq(test_routes.thing_path(nil, 5))
      end

      it "should treat undefined as non-given optional part" do
        expect(evaljs("Routes.thing_path(5, {optional_id: undefined})")).to eq(test_routes.thing_path(5, :optional_id => nil))
      end

      it "should raise error when passing non-full list of arguments and some query params" do
        expect { evaljs("Routes.thing_path(5, {q: 'hello'})") }
          .to raise_error(/Route missing required keys: id/)
      end

      it "should treat null as non-given optional part" do
        expect(evaljs("Routes.thing_path(5, {optional_id: null})")).to eq(test_routes.thing_path(5, :optional_id => nil))
      end

      it "should work when passing required params in options" do
        expect(evaljs("Routes.thing_deep_path({second_required: 1, third_required: 2})")).to eq(test_routes.thing_deep_path(second_required: 1, third_required: 2))
      end

      it "should skip leading and trailing optional parts" do
        expect(evaljs("Routes.thing_deep_path(1, 2)")).to eq(test_routes.thing_deep_path(1, 2))
      end
    end

    context "and including them" do
      it "should fail when insufficient arguments are given" do
        expect { evaljs("Routes.thing_deep_path(2)") }.to raise_error(/Route missing required keys: third_required/)
      end

      it "should include the optional parts" do
        expect(evaljs("Routes.things_path({optional_id: 5})")).to eq(test_routes.things_path(:optional_id => 5))
        expect(evaljs("Routes.things_path(5)")).to eq(test_routes.things_path(5))
        expect(evaljs("Routes.thing_deep_path(1, { third_required: 3, second_required: 2 })")).to eq(test_routes.thing_deep_path(1, third_required: 3, second_required: 2))
        expect(evaljs("Routes.thing_deep_path(1, { third_required: 3, second_required: 2, forth_optional: 4 })")).to eq(test_routes.thing_deep_path(1, third_required: 3, second_required: 2, forth_optional: 4))
        expect(evaljs("Routes.thing_deep_path(2, { third_required: 3, first_optional: 1 })")).to eq(test_routes.thing_deep_path(2, third_required: 3, first_optional: 1))
        expect(evaljs("Routes.thing_deep_path(3, { first_optional: 1, second_required: 2 })")).to eq(test_routes.thing_deep_path(3, first_optional: 1, second_required: 2))
        expect(evaljs("Routes.thing_deep_path(3, { first_optional: 1, second_required: 2, forth_optional: 4 })")).to eq(test_routes.thing_deep_path(3, first_optional: 1, second_required: 2, forth_optional: 4))
        expect(evaljs("Routes.thing_deep_path(4, { first_optional: 1, second_required: 2, third_required: 3 })")).to eq(test_routes.thing_deep_path(4, first_optional: 1, second_required: 2, third_required: 3))
        expect(evaljs("Routes.thing_deep_path(2, 3)")).to eq(test_routes.thing_deep_path(2, 3))
        expect(evaljs("Routes.thing_deep_path(1, 2, { third_required: 3 })")).to eq(test_routes.thing_deep_path(1, 2, third_required: 3))
        expect(evaljs("Routes.thing_deep_path(1,2, {third_required: 3, q: 'bogdan'})")).to eq(test_routes.thing_deep_path(1,2, {third_required: 3, q: 'bogdan'}))
        expect(evaljs("Routes.thing_deep_path(1, 2, { forth_optional: 4, third_required: 3 })")).to eq(test_routes.thing_deep_path(1, 2, forth_optional: 4, third_required: 3))
        expect(evaljs("Routes.thing_deep_path(1, 3, { second_required: 2 })")).to eq(test_routes.thing_deep_path(1, 3, second_required: 2))
        expect(evaljs("Routes.thing_deep_path(1, 4, { second_required: 2, third_required: 3 })")).to eq(test_routes.thing_deep_path(1, 4, second_required: 2, third_required: 3))
        expect(evaljs("Routes.thing_deep_path(2, 3, { first_optional: 1 })")).to eq(test_routes.thing_deep_path(2, 3, first_optional: 1))
        expect(evaljs("Routes.thing_deep_path(2, 3, { first_optional: 1, forth_optional: 4 })")).to eq(test_routes.thing_deep_path(2, 3, first_optional: 1, forth_optional: 4))
        expect(evaljs("Routes.thing_deep_path(2, 4, { first_optional: 1, third_required: 3 })")).to eq(test_routes.thing_deep_path(2, 4, first_optional: 1, third_required: 3))
        expect(evaljs("Routes.thing_deep_path(3, 4, { first_optional: 1, second_required: 2 })")).to eq(test_routes.thing_deep_path(3, 4, first_optional: 1, second_required: 2))
        expect(evaljs("Routes.thing_deep_path(1, 2, 3)")).to eq(test_routes.thing_deep_path(1, 2, 3))
        expect(evaljs("Routes.thing_deep_path(1, 2, 3, { forth_optional: 4 })")).to eq(test_routes.thing_deep_path(1, 2, 3, forth_optional: 4))
        expect(evaljs("Routes.thing_deep_path(1, 2, 4, { third_required: 3 })")).to eq(test_routes.thing_deep_path(1, 2, 4, third_required: 3))
        expect(evaljs("Routes.thing_deep_path(1, 3, 4, { second_required: 2 })")).to eq(test_routes.thing_deep_path(1, 3, 4, second_required: 2))
        expect(evaljs("Routes.thing_deep_path(2, 3, 4, { first_optional: 1 })")).to eq(test_routes.thing_deep_path(2, 3, 4, first_optional: 1))
        expect(evaljs("Routes.thing_deep_path(1, 2, 3, 4)")).to eq(test_routes.thing_deep_path(1, 2, 3, 4))

      end

      context "on nested optional parts" do
        if Rails.version <= "5.0.0"
          # this type of routing is deprecated
          it "should include everything that is not optional" do
            expect(evaljs("Routes.classic_path({controller: 'classic', action: 'edit'})")).to eq(test_routes.classic_path(controller: :classic, action: :edit))
          end
        end
      end
    end
  end

  context "when wrong parameters given" do

    it "should throw Exception if not enough parameters" do
      expect {
        evaljs("Routes.inbox_path()")
      }.to raise_error(js_error_class)
    end

    it "should throw Exception if required parameter is not defined" do
      expect {
        evaljs("Routes.inbox_path(null)")
      }.to raise_error(js_error_class)
    end

    it "should throw Exception if required parameter is not defined" do
      expect {
        evaljs("Routes.inbox_path(undefined)")
      }.to raise_error(js_error_class)
    end

    it "should throw Exceptions if when there is too many parameters" do
      expect {
        evaljs("Routes.inbox_path(1,2,3)")
      }.to raise_error(js_error_class)
    end
  end

  context "when javascript engine without Array#indexOf is used" do
    before(:each) do
      evaljs("Array.prototype.indexOf = null")
    end
    it "should still work correctly" do
      expect(evaljs("Routes.inboxes_path()")).to eq(test_routes.inboxes_path())
    end
  end

  context "when arguments are objects" do

    let(:klass) { Struct.new(:id, :to_param) }
    let(:inbox) { klass.new(1,"my") }

    it "should throw Exceptions if when pass id with null" do
      expect {
        evaljs("Routes.inbox_path({id: null})")
      }.to raise_error(js_error_class)
    end

    it "should throw Exceptions if when pass to_param with null" do
      expect {
        evaljs("Routes.inbox_path({to_param: null})")
      }.to raise_error(js_error_class)
    end
    it "should support 0 as a to_param option" do
      expect(evaljs("Routes.inbox_path({to_param: 0})")).to eq(test_routes.inbox_path(Struct.new(:to_param).new('0')))
    end

    it "should check for options special key" do
      expect(evaljs("Routes.inbox_path({id: 7, q: 'hello', _options: true})")).to eq(test_routes.inbox_path(id: 7, q: 'hello'))
      expect {
        evaljs("Routes.inbox_path({to_param: 7, _options: true})")
      }.to raise_error(js_error_class)
      expect(evaljs("Routes.inbox_message_path(5, {id: 7, q: 'hello', _options: true})")).to eq(test_routes.inbox_message_path(5, id: 7, q: 'hello'))
    end

    it "should support 0 as an id option" do
      expect(evaljs("Routes.inbox_path({id: 0})")).to eq(test_routes.inbox_path(0))
    end

    it "should use id property of the object in path" do
      expect(evaljs("Routes.inbox_path({id: 1})")).to eq(test_routes.inbox_path(1))
    end

    it "should prefer to_param property over id property" do
      expect(evaljs("Routes.inbox_path({id: 1, to_param: 'my'})")).to eq(test_routes.inbox_path(inbox))
    end

    it "should call to_param if it is a function" do
      expect(evaljs("Routes.inbox_path({id: 1, to_param: function(){ return 'my';}})")).to eq(test_routes.inbox_path(inbox))
    end

    it "should call id if it is a function" do
      expect(evaljs("Routes.inbox_path({id: function() { return 1;}})")).to eq(test_routes.inbox_path(1))
    end

    it "should support options argument" do
      expect(evaljs(
        "Routes.inbox_message_path({id:1, to_param: 'my'}, {id:2}, {custom: true, format: 'json'})"
      )).to eq(test_routes.inbox_message_path(inbox, 2, :custom => true, :format => "json"))
    end

    it "supports camel case property name" do
      expect(evaljs("Routes.inbox_path({id: 1, toParam: 'my'})")).to eq(test_routes.inbox_path(inbox))
    end

    it "supports camel case method name" do
      expect(evaljs("Routes.inbox_path({id: 1, toParam: function(){ return 'my';}})")).to eq(test_routes.inbox_path(inbox))
    end

    context "when globbing" do
      it "should prefer to_param property over id property" do
        expect(evaljs("Routes.book_path({id: 1, to_param: 'my'}, 1)")).to eq(test_routes.book_path(inbox, 1))
      end

      it "should call to_param if it is a function" do
        expect(evaljs("Routes.book_path({id: 1, to_param: function(){ return 'my';}}, 1)")).to eq(test_routes.book_path(inbox, 1))
      end

      it "should call id if it is a function" do
        expect(evaljs("Routes.book_path({id: function() { return 'technical';}}, 1)")).to eq(test_routes.book_path('technical', 1))
      end

      it "should support options argument" do
        expect(evaljs(
          "Routes.book_path({id:1, to_param: 'my'}, {id:2}, {custom: true, format: 'json'})"
        )).to eq(test_routes.book_path(inbox, 2, :custom => true, :format => "json"))
      end
    end

  end

  context "when specs" do
    it "should show inbox spec" do
      expect(evaljs("Routes.inbox_path.toString()")).to eq('/inboxes/:id(.:format)')
    end

    it "should show inbox spec convert to string" do
      expect(evaljs("'' + Routes.inbox_path")).to eq('/inboxes/:id(.:format)')
    end

    it "should show inbox message spec" do
      expect(evaljs("Routes.inbox_message_path.toString()")).to eq('/inboxes/:inbox_id/messages/:id(.:format)')
    end

    it "should show inbox message spec convert to string" do
      expect(evaljs("'' + Routes.inbox_message_path")).to eq('/inboxes/:inbox_id/messages/:id(.:format)')
    end
  end

  describe "requiredParams" do
    it "should show inbox spec" do
      expect(evaljs("Routes.inbox_path.requiredParams()").to_a).to eq(["id"])
    end

    it "should show inbox message spec" do
      expect(evaljs("Routes.inbox_message_path.requiredParams()").to_a).to eq(["inbox_id", "id"])
    end
  end
end