# frozen_string_literal: true

require "cases/helper"
require "support/schema_dumping_helper"

module PostgresqlUUIDHelper
  def connection
    @connection ||= ActiveRecord::Base.lease_connection
  end

  def drop_table(name)
    connection.drop_table name, if_exists: true
  end

  def uuid_function
    connection.supports_pgcrypto_uuid? ? "gen_random_uuid()" : "uuid_generate_v4()"
  end

  def uuid_default
    connection.supports_pgcrypto_uuid? ? {} : { default: uuid_function }
  end
end

class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase
  include PostgresqlUUIDHelper
  include SchemaDumpingHelper

  class UUIDType < ActiveRecord::Base
    self.table_name = "uuid_data_type"
  end

  setup do
    enable_extension!("uuid-ossp", connection)
    enable_extension!("pgcrypto",  connection) if connection.supports_pgcrypto_uuid?

    connection.create_table "uuid_data_type" do |t|
      t.uuid "guid"
    end
  end

  teardown do
    UUIDType.reset_column_information
    drop_table "uuid_data_type"
  end

  if ActiveRecord::Base.lease_connection.respond_to?(:supports_pgcrypto_uuid?) &&
      ActiveRecord::Base.lease_connection.supports_pgcrypto_uuid?
    def test_uuid_column_default
      connection.add_column :uuid_data_type, :thingy, :uuid, null: false, default: "gen_random_uuid()"
      UUIDType.reset_column_information
      column = UUIDType.columns_hash["thingy"]
      assert_equal "gen_random_uuid()", column.default_function
    end
  end

  def test_change_column_default
    connection.add_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v1()"
    UUIDType.reset_column_information
    column = UUIDType.columns_hash["thingy"]
    assert_equal "uuid_generate_v1()", column.default_function

    connection.change_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v4()"
    UUIDType.reset_column_information
    column = UUIDType.columns_hash["thingy"]
    assert_equal "uuid_generate_v4()", column.default_function
  ensure
    UUIDType.reset_column_information
  end

  def test_add_column_with_null_true_and_default_nil
    connection.add_column :uuid_data_type, :thingy, :uuid, null: true, default: nil

    UUIDType.reset_column_information
    column = UUIDType.columns_hash["thingy"]

    assert column.null
    assert_nil column.default
  end

  def test_add_column_with_default_array
    connection.add_column :uuid_data_type, :thingy, :uuid, array: true, default: []

    UUIDType.reset_column_information
    column = UUIDType.columns_hash["thingy"]

    assert_predicate column, :array?
    assert_equal "{}", column.default

    schema = dump_table_schema "uuid_data_type"
    assert_match %r{t\.uuid "thingy", default: \[\], array: true$}, schema
  end

  def test_data_type_of_uuid_types
    column = UUIDType.columns_hash["guid"]
    assert_equal :uuid, column.type
    assert_equal "uuid", column.sql_type
    assert_not_predicate column, :array?

    type = UUIDType.type_for_attribute("guid")
    assert_not_predicate type, :binary?
  end

  def test_treat_blank_uuid_as_nil
    UUIDType.create! guid: ""
    assert_nil(UUIDType.last.guid)
  end

  def test_treat_invalid_uuid_as_nil
    uuid = UUIDType.create! guid: "foobar"
    assert_nil(uuid.guid)
  end

  def test_invalid_uuid_dont_modify_before_type_cast
    uuid = UUIDType.new guid: "foobar"
    assert_equal "foobar", uuid.guid_before_type_cast
  end

  def test_invalid_uuid_dont_match_to_nil
    UUIDType.create!
    assert_empty UUIDType.where(guid: "")
    assert_empty UUIDType.where(guid: "foobar")
  end

  def test_uuid_change_format_does_not_mark_dirty
    model = UUIDType.create!(guid: "abcd-0123-4567-89ef-dead-beef-0101-1010")
    model.guid = model.guid.swapcase
    assert_not_predicate model, :changed?

    model.guid = "{#{model.guid}}"
    assert_not_predicate model, :changed?
  end

  class DuckUUID
    def initialize(uuid)
      @uuid = uuid
    end

    def to_s
      @uuid
    end
  end

  def test_acceptable_uuid_regex
    # Valid uuids
    ["A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11",
     "{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}",
     "a0eebc999c0b4ef8bb6d6bb9bd380a11",
     "a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11",
     "{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}",
     # The following is not a valid RFC 4122 UUID, but PG doesn't seem to care,
     # so we shouldn't block it either. (Pay attention to "fb6d" – the "f" here
     # is invalid – it must be one of 8, 9, A, B, a, b according to the spec.)
     "{a0eebc99-9c0b-4ef8-fb6d-6bb9bd380a11}",
     # Support Object-Oriented UUIDs which respond to #to_s
     DuckUUID.new("A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11"),
    ].each do |valid_uuid|
      uuid = UUIDType.new guid: valid_uuid
      assert_instance_of String, uuid.guid
    end

    # Invalid uuids
    [["A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11"],
     Hash.new,
     0,
     0.0,
     true,
     "Z0000C99-9C0B-4EF8-BB6D-6BB9BD380A11",
     "a0eebc999r0b4ef8ab6d6bb9bd380a11",
     "a0ee-bc99------4ef8-bb6d-6bb9-bd38-0a11",
     "{a0eebc99-bb6d6bb9-bd380a11}",
     "{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11",
     "a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}"].each do |invalid_uuid|
      uuid = UUIDType.new guid: invalid_uuid
      assert_nil uuid.guid
    end
  end

  def test_uuid_formats
    ["A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11",
     "{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}",
     "a0eebc999c0b4ef8bb6d6bb9bd380a11",
     "a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11",
     "{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}"].each do |valid_uuid|
      UUIDType.create(guid: valid_uuid)
      uuid = UUIDType.last
      assert_equal "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", uuid.guid
    end
  end

  def test_schema_dump_with_shorthand
    output = dump_table_schema "uuid_data_type"
    assert_match %r{t\.uuid "guid"}, output
  end

  def test_uniqueness_validation_ignores_uuid
    klass = Class.new(ActiveRecord::Base) do
      self.table_name = "uuid_data_type"
      validates :guid, uniqueness: { case_sensitive: false }

      def self.name
        "UUIDType"
      end
    end

    record = klass.create!(guid: "a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11")
    duplicate = klass.new(guid: record.guid)

    assert_predicate record.guid, :present? # Ensure we actually are testing a UUID
    assert_not_predicate duplicate, :valid?
  end
end

class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase
  include PostgresqlUUIDHelper
  include SchemaDumpingHelper

  class UUID < ActiveRecord::Base
    self.table_name = "pg_uuids"
  end

  setup do
    connection.create_table("pg_uuids", id: :uuid, default: "uuid_generate_v1()") do |t|
      t.string "name"
      t.uuid "other_uuid", default: "uuid_generate_v4()"
    end

    # Create custom PostgreSQL function to generate UUIDs
    # to test dumping tables which columns have defaults with custom functions
    connection.execute <<~SQL
      CREATE OR REPLACE FUNCTION my_uuid_generator() RETURNS uuid
      AS $$ SELECT * FROM #{uuid_function} $$
      LANGUAGE SQL VOLATILE;
    SQL

    # Create such a table with custom function as default value generator
    connection.create_table("pg_uuids_2", id: :uuid, default: "my_uuid_generator()") do |t|
      t.string "name"
      t.uuid "other_uuid_2", default: "my_uuid_generator()"
    end

    connection.create_table("pg_uuids_3", id: :uuid, **uuid_default) do |t|
      t.string "name"
    end
  end

  teardown do
    drop_table "pg_uuids"
    drop_table "pg_uuids_2"
    drop_table "pg_uuids_3"
    connection.execute "DROP FUNCTION IF EXISTS my_uuid_generator();"
  end

  def test_id_is_uuid
    assert_equal :uuid, UUID.columns_hash["id"].type
    assert UUID.primary_key
  end

  def test_id_has_a_default
    u = UUID.create
    assert_not_nil u.id
  end

  def test_auto_create_uuid
    u = UUID.create
    u.reload
    assert_not_nil u.other_uuid
  end

  def test_pk_and_sequence_for_uuid_primary_key
    pk, seq = connection.pk_and_sequence_for("pg_uuids")
    assert_equal "id", pk
    assert_nil seq
  end

  def test_schema_dumper_for_uuid_primary_key
    schema = dump_table_schema "pg_uuids"
    assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: -> { "uuid_generate_v1\(\)" }/, schema)
    assert_match(/t\.uuid "other_uuid", default: -> { "uuid_generate_v4\(\)" }/, schema)
  end

  def test_schema_dumper_for_uuid_primary_key_with_custom_default
    schema = dump_table_schema "pg_uuids_2"
    assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: -> { "my_uuid_generator\(\)" }/, schema)
    assert_match(/t\.uuid "other_uuid_2", default: -> { "my_uuid_generator\(\)" }/, schema)
  end

  def test_schema_dumper_for_uuid_primary_key_default
    schema = dump_table_schema "pg_uuids_3"
    if connection.supports_pgcrypto_uuid?
      assert_match(/\bcreate_table "pg_uuids_3", id: :uuid, default: -> { "gen_random_uuid\(\)" }/, schema)
    else
      assert_match(/\bcreate_table "pg_uuids_3", id: :uuid, default: -> { "uuid_generate_v4\(\)" }/, schema)
    end
  end

  def test_schema_dumper_for_uuid_primary_key_default_in_legacy_migration
    @verbose_was = ActiveRecord::Migration.verbose
    ActiveRecord::Migration.verbose = false

    migration = Class.new(ActiveRecord::Migration[5.0]) do
      def version; 101 end
      def migrate(x)
        create_table("pg_uuids_4", id: :uuid)
      end
    end.new

    pool = ActiveRecord::Base.connection_pool
    ActiveRecord::Migrator.new(:up, [migration], pool.schema_migration, pool.internal_metadata).migrate

    schema = dump_table_schema "pg_uuids_4"
    assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: -> { "uuid_generate_v4\(\)" }/, schema)
  ensure
    drop_table "pg_uuids_4"
    ActiveRecord::Migration.verbose = @verbose_was
    ActiveRecord::Base.connection_pool.schema_migration.delete_all_versions
  end
  uses_transaction :test_schema_dumper_for_uuid_primary_key_default_in_legacy_migration
end

class PostgresqlUUIDTestNilDefault < ActiveRecord::PostgreSQLTestCase
  include PostgresqlUUIDHelper
  include SchemaDumpingHelper

  setup do
    connection.create_table("pg_uuids", id: false) do |t|
      t.primary_key :id, :uuid, default: nil
      t.string "name"
    end
  end

  teardown do
    drop_table "pg_uuids"
  end

  def test_id_allows_default_override_via_nil
    col_desc = connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default
                                  FROM pg_attribute a
                                  LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
                                  WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first
    assert_nil col_desc["default"]
  end

  def test_schema_dumper_for_uuid_primary_key_with_default_override_via_nil
    schema = dump_table_schema "pg_uuids"
    assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: nil/, schema)
  end

  def test_schema_dumper_for_uuid_primary_key_with_default_nil_in_legacy_migration
    @verbose_was = ActiveRecord::Migration.verbose
    ActiveRecord::Migration.verbose = false

    migration = Class.new(ActiveRecord::Migration[5.0]) do
      def version; 101 end
      def migrate(x)
        create_table("pg_uuids_4", id: :uuid, default: nil)
      end
    end.new

    pool = ActiveRecord::Base.connection_pool
    ActiveRecord::Migrator.new(:up, [migration], pool.schema_migration, pool.internal_metadata).migrate

    schema = dump_table_schema "pg_uuids_4"
    assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: nil/, schema)
  ensure
    drop_table "pg_uuids_4"
    ActiveRecord::Migration.verbose = @verbose_was
    ActiveRecord::Base.connection_pool.schema_migration.delete_all_versions
  end
  uses_transaction :test_schema_dumper_for_uuid_primary_key_with_default_nil_in_legacy_migration
end

class PostgresqlUUIDTestInverseOf < ActiveRecord::PostgreSQLTestCase
  include PostgresqlUUIDHelper

  class UuidPost < ActiveRecord::Base
    self.table_name = "pg_uuid_posts"
    has_many :uuid_comments, inverse_of: :uuid_post
  end

  class UuidComment < ActiveRecord::Base
    self.table_name = "pg_uuid_comments"
    belongs_to :uuid_post
  end

  setup do
    connection.transaction do
      connection.create_table("pg_uuid_posts", id: :uuid, **uuid_default) do |t|
        t.string "title"
      end
      connection.create_table("pg_uuid_comments", id: :uuid, **uuid_default) do |t|
        t.references :uuid_post, type: :uuid
        t.string "content"
      end
    end
  end

  teardown do
    drop_table "pg_uuid_comments"
    drop_table "pg_uuid_posts"
  end

  def test_collection_association_with_uuid
    post    = UuidPost.create!
    comment = post.uuid_comments.create!
    assert post.uuid_comments.find(comment.id)
  end

  def test_find_with_uuid
    UuidPost.create!
    assert_raise ActiveRecord::RecordNotFound do
      UuidPost.find(123456)
    end
  end

  def test_find_by_with_uuid
    UuidPost.create!
    assert_nil UuidPost.find_by(id: 789)
  end
end

class PostgresqlUUIDHasManyThroughDisableJoinsTest < ActiveRecord::PostgreSQLTestCase
  include PostgresqlUUIDHelper

  class UuidForum < ActiveRecord::Base
    self.table_name = "pg_uuid_forums"
    has_many :uuid_posts, -> { order("title DESC") }
    has_many :uuid_comments, through: :uuid_posts
    has_many :uuid_comments_without_joins, through: :uuid_posts, source: :uuid_comments, disable_joins: true
  end

  class UuidPost < ActiveRecord::Base
    self.table_name = "pg_uuid_posts"
    belongs_to :uuid_forum
    has_many :uuid_comments
  end

  class UuidComment < ActiveRecord::Base
    self.table_name = "pg_uuid_comments"
    belongs_to :uuid_post
    has_one :uuid_forum, through: :uuid_post
    has_one :uuid_forum_without_joins, through: :uuid_post, source: :uuid_forum, disable_joins: true
  end

  setup do
    connection.transaction do
      connection.create_table("pg_uuid_forums", id: :uuid, **uuid_default) do |t|
        t.string "name"
      end
      connection.create_table("pg_uuid_posts", id: :uuid, **uuid_default) do |t|
        t.references :uuid_forum, type: :uuid
        t.string "title"
      end
      connection.create_table("pg_uuid_comments", id: :uuid, **uuid_default) do |t|
        t.references :uuid_post, type: :uuid
        t.string "content"
      end
    end
  end

  teardown do
    drop_table "pg_uuid_comments"
    drop_table "pg_uuid_posts"
    drop_table "pg_uuid_forums"
  end

  def test_uuid_primary_key_and_disable_joins_with_delegate_cache
    uuid_forum = UuidForum.create!
    uuid_post_1 = uuid_forum.uuid_posts.create!
    uuid_comment_1_1 = uuid_post_1.uuid_comments.create!
    uuid_comment_1_2 = uuid_post_1.uuid_comments.create!
    uuid_post_2 = uuid_forum.uuid_posts.create!
    uuid_comment_2_1 = uuid_post_2.uuid_comments.create!
    uuid_comment_2_2 = uuid_post_2.uuid_comments.create!
    uuid_comment_2_3 = uuid_post_2.uuid_comments.create!

    assert_equal uuid_forum.uuid_comments_without_joins.order(:id).to_a.map(&:id).sort,
      [uuid_comment_1_1.id, uuid_comment_1_2.id, uuid_comment_2_1.id, uuid_comment_2_2.id, uuid_comment_2_3.id].sort
  end
end
