require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")

Sequel.extension :pg_array, :pg_json

describe "pg_json extension" do
  before(:all) do
    m = Sequel::Postgres
    @m = m::JSONDatabaseMethods
    @hc = m::JSONHash
    @ac = m::JSONArray
    @bhc = m::JSONBHash
    @bac = m::JSONBArray
  end
  before do
    @db = Sequel.connect('mock://postgres')
    @db.extend_datasets{def quote_identifiers?; false end}
    @db.extension(:pg_array, :pg_json)
  end

  deprecated "should set up conversion procs correctly" do
    cp = Sequel::Postgres::PG__TYPES
    cp[114].call("{}").must_equal @hc.new({})
    cp[3802].call("{}").must_equal @bhc.new({})
  end

  deprecated "should set up conversion procs for arrays correctly" do
    cp = Sequel::Postgres::PG__TYPES
    cp[199].call("{[]}").must_equal [@ac.new([])]
    cp[3807].call("{[]}").must_equal [@bac.new([])]
  end

  it "should set up conversion procs correctly" do
    cp = @db.conversion_procs
    cp[114].call("{}").must_equal @hc.new({})
    cp[3802].call("{}").must_equal @bhc.new({})
  end

  it "should set up conversion procs for arrays correctly" do
    cp = @db.conversion_procs
    cp[199].call("{[]}").must_equal [@ac.new([])]
    cp[3807].call("{[]}").must_equal [@bac.new([])]
  end

  it "should parse json strings correctly" do
    @m.parse_json('[]').class.must_equal(@ac)
    @m.parse_json('[]').to_a.must_equal []
    @m.parse_json('[1]').to_a.must_equal [1]
    @m.parse_json('[1, 2]').to_a.must_equal [1, 2]
    @m.parse_json('[1, [2], {"a": "b"}]').to_a.must_equal [1, [2], {'a'=>'b'}]
    @m.parse_json('{}').class.must_equal(@hc)
    @m.parse_json('{}').to_hash.must_equal({})
    @m.parse_json('{"a": "b"}').to_hash.must_equal('a'=>'b')
    @m.parse_json('{"a": "b", "c": [1, 2, 3]}').to_hash.must_equal('a'=>'b', 'c'=>[1, 2, 3])
    @m.parse_json('{"a": "b", "c": {"d": "e"}}').to_hash.must_equal('a'=>'b', 'c'=>{'d'=>'e'})
  end

  it "should parse json and non-json plain strings, integers, and floats correctly in db_parse_json" do
    @m.db_parse_json('{"a": "b", "c": {"d": "e"}}').to_hash.must_equal('a'=>'b', 'c'=>{'d'=>'e'})
    @m.db_parse_json('[1, [2], {"a": "b"}]').to_a.must_equal [1, [2], {'a'=>'b'}]
    @m.db_parse_json('1').must_equal 1
    @m.db_parse_json('"b"').must_equal 'b'
    @m.db_parse_json('1.1').must_equal 1.1
  end

  it "should parse json and non-json plain strings, integers, and floats correctly in db_parse_jsonb" do
    @m.db_parse_jsonb('{"a": "b", "c": {"d": "e"}}').to_hash.must_equal('a'=>'b', 'c'=>{'d'=>'e'})
    @m.db_parse_jsonb('[1, [2], {"a": "b"}]').to_a.must_equal [1, [2], {'a'=>'b'}]
    @m.db_parse_jsonb('1').must_equal 1
    @m.db_parse_jsonb('"b"').must_equal 'b'
    @m.db_parse_jsonb('1.1').must_equal 1.1
  end

  it "should raise an error when attempting to parse invalid json" do
    proc{@m.parse_json('')}.must_raise(Sequel::InvalidValue)
    proc{@m.parse_json('a')}.must_raise(Sequel::InvalidValue)

    begin
      Sequel.instance_eval do
        alias pj parse_json
        def parse_json(v)
          {'1'=>1, "'a'"=>'a', 'true'=>true, 'false'=>false, 'null'=>nil, 'o'=>Object.new, '[one]'=>[1]}.fetch(v){pj(v)}
        end
      end
      @m.parse_json('1').must_equal 1
      @m.parse_json("'a'").must_equal 'a'
      @m.parse_json('true').must_equal true
      @m.parse_json('false').must_equal false
      @m.parse_json('null').must_be_nil
      proc{@m.parse_json('o')}.must_raise(Sequel::InvalidValue)
      @m.db_parse_json('one').must_equal 1
      @m.db_parse_jsonb('one').must_equal 1
    ensure
      Sequel.instance_eval do
        alias parse_json pj
      end
    end
  end

  it "should literalize JSONHash and JSONArray to strings correctly" do
    @db.literal(Sequel.pg_json([])).must_equal "'[]'::json"
    @db.literal(Sequel.pg_json([1, [2], {'a'=>'b'}])).must_equal "'[1,[2],{\"a\":\"b\"}]'::json"
    @db.literal(Sequel.pg_json({})).must_equal "'{}'::json"
    @db.literal(Sequel.pg_json('a'=>'b')).must_equal "'{\"a\":\"b\"}'::json"
  end

  it "should literalize JSONHash and JSONArray to strings correctly" do
    @db.literal(Sequel.pg_jsonb([])).must_equal "'[]'::jsonb"
    @db.literal(Sequel.pg_jsonb([1, [2], {'a'=>'b'}])).must_equal "'[1,[2],{\"a\":\"b\"}]'::jsonb"
    @db.literal(Sequel.pg_jsonb({})).must_equal "'{}'::jsonb"
    @db.literal(Sequel.pg_jsonb('a'=>'b')).must_equal "'{\"a\":\"b\"}'::jsonb"
  end

  it "should have Sequel.pg_json return JSONHash and JSONArray as is" do
    a = Sequel.pg_json({})
    Sequel.pg_json(a).object_id.must_equal(a.object_id)
    a = Sequel.pg_json([])
    Sequel.pg_json(a).object_id.must_equal(a.object_id)
  end

  it "should have Sequel.pg_json convert jsonb values" do
    a = {}
    v = Sequel.pg_json(Sequel.pg_jsonb(a))
    v.to_hash.must_be_same_as(a)
    v.class.must_equal(@hc)

    a = []
    v = Sequel.pg_json(Sequel.pg_jsonb(a))
    v.to_a.must_be_same_as(a)
    v.class.must_equal(@ac)
  end

  it "should have Sequel.pg_jsonb return JSONBHash and JSONBArray as is" do
    a = Sequel.pg_jsonb({})
    Sequel.pg_jsonb(a).object_id.must_equal(a.object_id)
    a = Sequel.pg_jsonb([])
    Sequel.pg_jsonb(a).object_id.must_equal(a.object_id)
  end

  it "should have Sequel.pg_jsonb convert json values" do
    a = {}
    v = Sequel.pg_jsonb(Sequel.pg_json(a))
    v.to_hash.must_be_same_as(a)
    v.class.must_equal(@bhc)

    a = []
    v = Sequel.pg_jsonb(Sequel.pg_json(a))
    v.to_a.must_be_same_as(a)
    v.class.must_equal(@bac)
  end

  it "should have JSONHashBase#to_hash method for getting underlying hash" do
    Sequel.pg_json({}).to_hash.must_be_kind_of(Hash)
    Sequel.pg_jsonb({}).to_hash.must_be_kind_of(Hash)
  end

  it "should allow aliasing json objects" do
    @db.literal(Sequel.pg_json({}).as(:a)).must_equal "'{}'::json AS a"
    @db.literal(Sequel.pg_json([]).as(:a)).must_equal "'[]'::json AS a"
    @db.literal(Sequel.pg_jsonb({}).as(:a)).must_equal "'{}'::jsonb AS a"
    @db.literal(Sequel.pg_jsonb([]).as(:a)).must_equal "'[]'::jsonb AS a"
  end

  it "should allow casting json objects" do
    @db.literal(Sequel.pg_json({}).cast(String)).must_equal "CAST('{}'::json AS text)"
    @db.literal(Sequel.pg_json([]).cast(String)).must_equal "CAST('[]'::json AS text)"
    @db.literal(Sequel.pg_jsonb({}).cast(String)).must_equal "CAST('{}'::jsonb AS text)"
    @db.literal(Sequel.pg_jsonb([]).cast(String)).must_equal "CAST('[]'::jsonb AS text)"
  end

  it "should have JSONArrayBase#to_a method for getting underlying array" do
    Sequel.pg_json([]).to_a.must_be_kind_of(Array)
    Sequel.pg_jsonb([]).to_a.must_be_kind_of(Array)
  end

  it "should support using JSONHashBase and JSONArrayBase as bound variables" do
    @db.bound_variable_arg(1, nil).must_equal 1
    @db.bound_variable_arg(Sequel.pg_json([1]), nil).must_equal '[1]'
    @db.bound_variable_arg(Sequel.pg_json('a'=>'b'), nil).must_equal '{"a":"b"}'
    @db.bound_variable_arg(Sequel.pg_jsonb([1]), nil).must_equal '[1]'
    @db.bound_variable_arg(Sequel.pg_jsonb('a'=>'b'), nil).must_equal '{"a":"b"}'
  end

  it "should support using json[] and jsonb[] types in bound variables" do
    @db.bound_variable_arg(Sequel.pg_array([Sequel.pg_json([{"a"=>1}]), Sequel.pg_json("b"=>[1, 2])]), nil).must_equal '{"[{\\"a\\":1}]","{\\"b\\":[1,2]}"}'
    @db.bound_variable_arg(Sequel.pg_array([Sequel.pg_jsonb([{"a"=>1}]), Sequel.pg_jsonb("b"=>[1, 2])]), nil).must_equal '{"[{\\"a\\":1}]","{\\"b\\":[1,2]}"}'
  end

  it "should parse json type from the schema correctly" do
    @db.fetch = [{:name=>'id', :db_type=>'integer'}, {:name=>'i', :db_type=>'json'}]
    @db.schema(:items).map{|e| e[1][:type]}.must_equal [:integer, :json]
  end

  it "should parse json type from the schema correctly" do
    @db.fetch = [{:name=>'id', :db_type=>'integer'}, {:name=>'i', :db_type=>'jsonb'}]
    @db.schema(:items).map{|e| e[1][:type]}.must_equal [:integer, :jsonb]
  end

  it "should support typecasting for the json type" do
    h = Sequel.pg_json(1=>2)
    a = Sequel.pg_json([1])
    @db.typecast_value(:json, h).object_id.must_equal(h.object_id)
    @db.typecast_value(:json, h.to_hash).must_equal h
    @db.typecast_value(:json, h.to_hash).class.must_equal(@hc)
    @db.typecast_value(:json, Sequel.pg_jsonb(h)).must_equal h
    @db.typecast_value(:json, Sequel.pg_jsonb(h)).class.must_equal(@hc)
    @db.typecast_value(:json, a).object_id.must_equal(a.object_id)
    @db.typecast_value(:json, a.to_a).must_equal a
    @db.typecast_value(:json, a.to_a).class.must_equal(@ac)
    @db.typecast_value(:json, Sequel.pg_jsonb(a)).must_equal a
    @db.typecast_value(:json, Sequel.pg_jsonb(a)).class.must_equal(@ac)
    @db.typecast_value(:json, '[]').must_equal Sequel.pg_json([])
    @db.typecast_value(:json, '[]').class.must_equal(@ac)
    @db.typecast_value(:json, '{"a": "b"}').must_equal Sequel.pg_json("a"=>"b")
    @db.typecast_value(:json, '{"a": "b"}').class.must_equal(@hc)
    proc{@db.typecast_value(:json, '')}.must_raise(Sequel::InvalidValue)
    proc{@db.typecast_value(:json, 1)}.must_raise(Sequel::InvalidValue)
  end

  it "should support typecasting for the jsonb type" do
    h = Sequel.pg_jsonb(1=>2)
    a = Sequel.pg_jsonb([1])
    @db.typecast_value(:jsonb, h).object_id.must_equal(h.object_id)
    @db.typecast_value(:jsonb, h.to_hash).must_equal h
    @db.typecast_value(:jsonb, h.to_hash).class.must_equal(@bhc)
    @db.typecast_value(:jsonb, Sequel.pg_json(h)).must_equal h
    @db.typecast_value(:jsonb, Sequel.pg_json(h)).class.must_equal(@bhc)
    @db.typecast_value(:jsonb, a).object_id.must_equal(a.object_id)
    @db.typecast_value(:jsonb, a.to_a).must_equal a
    @db.typecast_value(:jsonb, a.to_a).class.must_equal(@bac)
    @db.typecast_value(:jsonb, Sequel.pg_json(a)).must_equal a
    @db.typecast_value(:jsonb, Sequel.pg_json(a)).class.must_equal(@bac)
    @db.typecast_value(:jsonb, '[]').must_equal Sequel.pg_jsonb([])
    @db.typecast_value(:jsonb, '[]').class.must_equal(@bac)
    @db.typecast_value(:jsonb, '{"a": "b"}').must_equal Sequel.pg_jsonb("a"=>"b")
    @db.typecast_value(:jsonb, '{"a": "b"}').class.must_equal(@bhc)
    proc{@db.typecast_value(:jsonb, '')}.must_raise(Sequel::InvalidValue)
    proc{@db.typecast_value(:jsonb, 1)}.must_raise(Sequel::InvalidValue)
  end

  it "should return correct results for Database#schema_type_class" do
    @db.schema_type_class(:json).must_equal [Sequel::Postgres::JSONHash, Sequel::Postgres::JSONArray]
    @db.schema_type_class(:jsonb).must_equal [Sequel::Postgres::JSONBHash, Sequel::Postgres::JSONBArray]
    @db.schema_type_class(:integer).must_equal Integer
  end
end
