require "spec"
require "uuid"
require "spec/helpers/string"
require "../support/wasm32"

describe "UUID" do
  describe "#==" do
    it "matches identical UUIDs" do
      UUID.new("50a11da6-377b-4bdf-b9f0-076f9db61c93").should eq UUID.new("50a11da6-377b-4bdf-b9f0-076f9db61c93")
      UUID.new("50a11da6-377b-4bdf-b9f0-076f9db61c93").hash.should eq UUID.new("50a11da6-377b-4bdf-b9f0-076f9db61c93").hash
    end
  end

  describe "#<=>" do
    it "correctly compares two UUIDs" do
      uuid_1 = UUID.new("00330000-0000-0000-0000-000000000000")
      uuid_2 = UUID.new("00000011-0000-0000-5500-000099000000")
      (uuid_1 <=> uuid_2).should be > 0
      (uuid_2 <=> uuid_1).should be < 0
      (uuid_1 <=> uuid_1).should eq 0
    end
  end

  describe "random initialize" do
    it "works with no options" do
      subject = UUID.random
      subject.variant.should eq UUID::Variant::RFC4122
      subject.version.should eq UUID::Version::V4
    end

    it "does inspect" do
      subject = UUID.random
      subject.inspect.should eq "UUID(#{subject})"
    end

    it "works with variant" do
      subject = UUID.random(variant: UUID::Variant::NCS)
      subject.variant.should eq UUID::Variant::NCS
      subject.version.should eq UUID::Version::V4
    end

    it "works with version" do
      subject = UUID.random(version: UUID::Version::V3)
      subject.variant.should eq UUID::Variant::RFC4122
      subject.version.should eq UUID::Version::V3
    end
  end

  describe "initialize from static array" do
    it "works with static array only" do
      subject = UUID.new(StaticArray(UInt8, 16).new(0_u8))
      subject.to_s.should eq "00000000-0000-0000-0000-000000000000"
    end

    it "works with static array and variant" do
      subject = UUID.new(StaticArray(UInt8, 16).new(0_u8), variant: UUID::Variant::RFC4122)
      subject.to_s.should eq "00000000-0000-0000-8000-000000000000"
      subject.variant.should eq UUID::Variant::RFC4122
    end

    it "works with static array and version" do
      subject = UUID.new(StaticArray(UInt8, 16).new(0_u8), version: UUID::Version::V3)
      subject.to_s.should eq "00000000-0000-3000-0000-000000000000"
      subject.version.should eq UUID::Version::V3
    end

    it "works with static array, variant and version" do
      subject = UUID.new(StaticArray(UInt8, 16).new(0_u8), variant: UUID::Variant::Microsoft, version: UUID::Version::V3)
      subject.to_s.should eq "00000000-0000-3000-c000-000000000000"
      subject.variant.should eq UUID::Variant::Microsoft
      subject.version.should eq UUID::Version::V3
    end
  end

  it "initializes with slice" do
    subject = UUID.new(Slice(UInt8).new(16, 0_u8), variant: UUID::Variant::RFC4122, version: UUID::Version::V4)
    subject.to_s.should eq "00000000-0000-4000-8000-000000000000"
    subject.variant.should eq UUID::Variant::RFC4122
    subject.version.should eq UUID::Version::V4
  end

  describe "initialize with String" do
    it "works with string only" do
      subject = UUID.new("00000000-0000-0000-0000-000000000000")
      subject.to_s.should eq "00000000-0000-0000-0000-000000000000"
    end

    it "works with string and variant" do
      subject = UUID.new("00000000-0000-0000-0000-000000000000", variant: UUID::Variant::Future)
      subject.to_s.should eq "00000000-0000-0000-e000-000000000000"
      subject.variant.should eq UUID::Variant::Future
    end

    it "works with string and version" do
      subject = UUID.new("00000000-0000-0000-0000-000000000000", version: UUID::Version::V5)
      subject.to_s.should eq "00000000-0000-5000-0000-000000000000"
      subject.version.should eq UUID::Version::V5
    end

    it "can be built from strings" do
      UUID.new("c20335c3-7f46-4126-aae9-f665434ad12b").to_s.should eq("c20335c3-7f46-4126-aae9-f665434ad12b")
      UUID.new("c20335c37f464126aae9f665434ad12b").to_s.should eq("c20335c3-7f46-4126-aae9-f665434ad12b")
      UUID.new("C20335C3-7F46-4126-AAE9-F665434AD12B").to_s.should eq("c20335c3-7f46-4126-aae9-f665434ad12b")
      UUID.new("C20335C37F464126AAE9F665434AD12B").to_s.should eq("c20335c3-7f46-4126-aae9-f665434ad12b")
      UUID.new("urn:uuid:1ed1ee2f-ef9a-4f9c-9615-ab14d8ef2892").to_s.should eq("1ed1ee2f-ef9a-4f9c-9615-ab14d8ef2892")
    end
  end

  describe "parsing strings" do
    it "returns a properly parsed UUID" do
      UUID.parse?("c20335c3-7f46-4126-aae9-f665434ad12b").to_s.should eq("c20335c3-7f46-4126-aae9-f665434ad12b")
    end

    it "returns nil if it has the wrong number of characters" do
      UUID.parse?("nope").should be_nil
    end

    it "returns nil if it has incorrect characters" do
      UUID.parse?("c20335c3-7f46-4126-aae9-f665434ad12?").should be_nil
      UUID.parse?("lol!wut?-asdf-fork-typo-omglolwtfbbq").should be_nil
      UUID.parse?("lol!wut?asdfforktypoomglolwtfbbq").should be_nil
      UUID.parse?("urn:uuid:lol!wut?-asdf-fork-typo-omglolwtfbbq").should be_nil
    end
  end

  it "initializes from UUID" do
    uuid = UUID.new("50a11da6-377b-4bdf-b9f0-076f9db61c93")
    uuid = UUID.new(uuid, version: UUID::Version::V2, variant: UUID::Variant::Microsoft)
    uuid.version.should eq UUID::Version::V2
    uuid.variant.should eq UUID::Variant::Microsoft
    uuid.bytes.should eq(UInt8.static_array(80, 161, 29, 166, 55, 123, 43, 223, 217, 240, 7, 111, 157, 182, 28, 147))
    uuid.to_s.should eq "50a11da6-377b-2bdf-d9f0-076f9db61c93"
  end

  it "initializes zeroed UUID" do
    UUID.empty.should eq UUID.new(StaticArray(UInt8, 16).new(0_u8), UUID::Variant::NCS, UUID::Version::V4)
    UUID.empty.to_s.should eq "00000000-0000-4000-0000-000000000000"
    UUID.empty.variant.should eq UUID::Variant::NCS
    UUID.empty.version.should eq UUID::Version::V4
  end

  describe "supports different string formats" do
    it "normal output" do
      assert_prints UUID.new("ee843b2656d8472bb3430b94ed9077ff").to_s, "ee843b26-56d8-472b-b343-0b94ed9077ff"
    end

    it "hexstring" do
      UUID.new("3e806983-eca4-4fc5-b581-f30fb03ec9e5").hexstring.should eq "3e806983eca44fc5b581f30fb03ec9e5"
    end

    it "urn" do
      UUID.new("1ed1ee2f-ef9a-4f9c-9615-ab14d8ef2892").urn.should eq "urn:uuid:1ed1ee2f-ef9a-4f9c-9615-ab14d8ef2892"
    end
  end

  it "fails on invalid arguments when creating" do
    expect_raises(ArgumentError) { UUID.new "25d6f843?cf8e-44fb-9f84-6062419c4330" }
    expect_raises(ArgumentError) { UUID.new "67dc9e24-0865 474b-9fe7-61445bfea3b5" }
    expect_raises(ArgumentError) { UUID.new "5942cde5-10d1-416b+85c4-9fc473fa1037" }
    expect_raises(ArgumentError) { UUID.new "0f02a229-4898-4029-926f=94be5628a7fd" }
    expect_raises(ArgumentError) { UUID.new "cda08c86-6413-474f-8822-a6646e0fb19G" }
    expect_raises(ArgumentError) { UUID.new "2b1bfW06368947e59ac07c3ffdaf514c" }
    expect_raises(ArgumentError) { UUID.new "xyz:uuid:1ed1ee2f-ef9a-4f9c-9615-ab14d8ef2892" }
  end

  describe "v1" do
    it "returns true if UUID is v1, false otherwise" do
      uuid = UUID.v1
      uuid.v1?.should be_true
      uuid = UUID.v1(node_id: StaticArray(UInt8, 6).new { |i| (i*10).to_u8 })
      uuid.to_s[24..36].should eq("000a141e2832")
    end
  end

  describe "v2" do
    it "returns true if UUID is v2, false otherwise" do
      uuid = UUID.v2(UUID::Domain::Person, 42)
      uuid.v2?.should be_true
      uuid = UUID.v2(UUID::Domain::Person, 42, node_id: StaticArray(UInt8, 6).new { |i| (i*10).to_u8 })
      uuid.to_s[24..36].should eq("000a141e2832")
    end
  end

  describe "v4?" do
    it "returns true if UUID is v4, false otherwise" do
      uuid = UUID.random
      uuid.v4?.should be_true
      uuid = UUID.v4
      uuid.v4?.should be_true
      uuid = UUID.new("00000000-0000-0000-0000-000000000000", version: UUID::Version::V5)
      uuid.v4?.should be_false
    end
  end

  describe "v4!" do
    it "returns true if UUID is v4, raises otherwise" do
      uuid = UUID.random
      uuid.v4!.should be_true
      uuid = UUID.v4
      uuid.v4!.should be_true
      uuid = UUID.new("00000000-0000-0000-0000-000000000000", version: UUID::Version::V5)
      expect_raises(UUID::Error) { uuid.v4! }
    end
  end

  describe "v3" do
    it "generates DNS based names correctly" do
      data = "crystal-lang.org"
      expected = "60a4b7b5-3333-3f1e-a2cd-30d8a2d0b83b"
      UUID.v3(data, UUID::Namespace::DNS).to_s.should eq(expected)
      UUID.v3_dns(data).to_s.should eq(expected)
      UUID.v3_dns(data).v3?.should be_true
    end

    it "generates URL based names correctly" do
      data = "https://crystal-lang.org"
      expected = "c25c7b79-5f5f-3844-98a4-2548f5d0e7f9"
      UUID.v3(data, UUID::Namespace::URL).to_s.should eq(expected)
      UUID.v3_url(data).to_s.should eq(expected)
      UUID.v3_url(data).v3?.should be_true
    end

    it "generates OID based names correctly" do
      data = "1.3.6.1.4.1.343"
      expected = "77bc1dc3-0a9f-3e7e-bfa5-3f611a660c80"
      UUID.v3(data, UUID::Namespace::OID).to_s.should eq(expected)
      UUID.v3_oid(data).to_s.should eq(expected)
      UUID.v3_oid(data).v3?.should be_true
    end

    it "generates X500 based names correctly" do
      data = "cn=John Doe, ou=People, o=example, c=com"
      expected = "fcab1a4c-fc81-3ebc-9874-9a8b931911d3"
      UUID.v3(data, UUID::Namespace::X500).to_s.should eq(expected)
      UUID.v3_x500(data).to_s.should eq(expected)
      UUID.v3_x500(data).v3?.should be_true
    end
  end

  describe "v5" do
    it "generates DNS based names correctly" do
      data = "crystal-lang.org"
      expected = "11caf27c-b803-5e62-9c4b-15332b04047e"
      UUID.v5(data, UUID::Namespace::DNS).to_s.should eq(expected)
      UUID.v5_dns(data).to_s.should eq(expected)
      UUID.v5_dns(data).v5?.should be_true
    end

    it "generates URL based names correctly" do
      data = "https://crystal-lang.org"
      expected = "29fec3f0-9ad0-5e8a-a42e-214ff695f50e"
      UUID.v5(data, UUID::Namespace::URL).to_s.should eq(expected)
      UUID.v5_url(data).to_s.should eq(expected)
      UUID.v5_url(data).v5?.should be_true
    end

    it "generates OID based names correctly" do
      data = "1.3.6.1.4.1.343"
      expected = "6aab0456-7392-582a-b92a-ba5a7096945d"
      UUID.v5(data, UUID::Namespace::OID).to_s.should eq(expected)
      UUID.v5_oid(data).to_s.should eq(expected)
      UUID.v5_oid(data).v5?.should be_true
    end

    it "generates X500 based names correctly" do
      data = "cn=John Doe, ou=People, o=example, c=com"
      expected = "bc10b2d9-f370-5c65-9561-5e3f6d9b236d"
      UUID.v5(data, UUID::Namespace::X500).to_s.should eq(expected)
      UUID.v5_x500(data).to_s.should eq(expected)
      UUID.v5_x500(data).v5?.should be_true
    end
  end

  describe "v6" do
    it "generates a v6 UUID" do
      uuid = UUID.v6
      uuid.v6?.should be_true
      uuid.version.should eq UUID::Version::V6
      uuid.variant.should eq UUID::Variant::RFC4122
    end

    it "accepts a custom node_id" do
      uuid = UUID.v6(node_id: StaticArray(UInt8, 6).new { |i| (i * 10).to_u8 })
      uuid.v6?.should be_true
      uuid.to_s[24..36].should eq("000a141e2832")
    end

    it "accepts a custom clock_seq" do
      uuid = UUID.v6(clock_seq: 0x1234_u16)
      uuid.v6?.should be_true
      # clock_seq occupies the low 14 bits of bytes 8-9 after the 2 variant bits
      clock_seq = ((uuid.bytes[8].to_u16 & 0x3f) << 8) | uuid.bytes[9].to_u16
      clock_seq.should eq 0x1234_u16
    end

    pending_wasm32 "generates UUIDs that are sortable" do
      uuids = Array.new(10) do
        sleep 1.millisecond
        UUID.v6
      end

      uuids.should eq uuids.sort
    end
  end

  describe "v7" do
    it "generates a v7 UUID" do
      uuid = UUID.v7
      uuid.v7?.should be_true
      uuid.variant.rfc9562?.should be_true
    end

    pending_wasm32 "generates UUIDs that are sortable with 1ms precision" do
      uuids = Array.new(10) do
        sleep 1.millisecond
        UUID.v7
      end

      uuids.should eq uuids.sort
    end
  end

  describe "v8" do
    it "generates a v8 UUID from a StaticArray" do
      custom = StaticArray(UInt8, 16).new { |i| i.to_u8 }
      uuid = UUID.v8(custom)
      uuid.v8?.should be_true
      uuid.version.should eq UUID::Version::V8
      uuid.variant.should eq UUID::Variant::RFC4122
    end

    it "generates a v8 UUID from a Slice" do
      custom = Bytes.new(16) { |i| i.to_u8 }
      uuid = UUID.v8(custom)
      uuid.v8?.should be_true
      uuid.version.should eq UUID::Version::V8
      uuid.variant.should eq UUID::Variant::RFC4122
    end

    it "preserves user payload except version and variant bits" do
      custom = StaticArray(UInt8, 16).new(0xff_u8)
      uuid = UUID.v8(custom)
      # All bytes except 6 (version nibble) and 8 (variant bits) should be 0xFF
      # byte 6: high nibble = 8 (version), low nibble = 0xf (preserved)
      # byte 8: high 2 bits = 10 (variant), low 6 bits = 0x3f (preserved)
      uuid.bytes.should eq UInt8.static_array(
        0xff, 0xff, 0xff, 0xff,
        0xff, 0xff, 0x8f, 0xff,
        0xbf, 0xff, 0xff, 0xff,
        0xff, 0xff, 0xff, 0xff,
      )
    end

    it "raises on a Slice with wrong size" do
      expect_raises(ArgumentError) { UUID.v8(Bytes.new(10)) }
    end
  end
end
