defmodule RDF.XSD.Datatype.Test.Case do
  @moduledoc """
  `ExUnit.CaseTemplate` for test of `RDF.XSD.Datatype`s.
  """

  use ExUnit.CaseTemplate

  alias RDF.XSD

  using(opts) do
    datatype = Keyword.fetch!(opts, :datatype)
    datatype_name = Keyword.fetch!(opts, :name)

    datatype_iri = Keyword.get(opts, :iri, RDF.NS.XSD.__base_iri__() <> datatype_name)

    valid = Keyword.get(opts, :valid)
    invalid = Keyword.get(opts, :invalid)
    primitive = Keyword.get(opts, :primitive)
    base = unless primitive, do: Keyword.fetch!(opts, :base)
    base_primitive = unless primitive, do: Keyword.fetch!(opts, :base_primitive)
    applicable_facets = Keyword.get(opts, :applicable_facets, [])
    facets = Keyword.get(opts, :facets)

    # credo:disable-for-next-line Credo.Check.Refactor.LongQuoteBlocks
    quote generated: true do
      alias RDF.XSD
      alias RDF.XSD.Datatype
      alias RDF.TestDatatypes.{Age, DecimalUnitInterval, DoubleUnitInterval, FloatUnitInterval}
      alias unquote(datatype)
      import unquote(__MODULE__)

      doctest unquote(datatype)

      @moduletag datatype: unquote(datatype)

      if unquote(valid) do
        @valid unquote(valid)
        @invalid unquote(invalid)

        test "registration" do
          assert unquote(datatype) in RDF.Literal.Datatype.Registry.builtin_datatypes()
          assert unquote(datatype) in RDF.Literal.Datatype.Registry.builtin_xsd_datatypes()

          assert unquote(datatype) |> RDF.Literal.Datatype.Registry.builtin_datatype?()
          assert unquote(datatype) |> RDF.Literal.Datatype.Registry.builtin_xsd_datatype?()

          assert RDF.Literal.Datatype.get(unquote(datatype_iri)) == unquote(datatype)
          assert XSD.Datatype.get(unquote(datatype_iri)) == unquote(datatype)
        end

        test "primitive/0" do
          assert unquote(datatype).primitive?() == unquote(!!primitive)
        end

        test "base/0" do
          if unquote(primitive) do
            assert unquote(datatype).base == nil
          else
            assert unquote(datatype).base == unquote(base)
          end
        end

        test "base_primitive/0" do
          if unquote(primitive) do
            assert unquote(datatype).base_primitive == unquote(datatype)
          else
            assert unquote(datatype).base_primitive == unquote(base_primitive)
          end
        end

        test "derived_from?/1" do
          assert unquote(datatype).derived_from?(unquote(datatype)) == false

          unless unquote(primitive) do
            assert unquote(datatype).derived_from?(unquote(base)) == true
            assert unquote(datatype).derived_from?(unquote(base_primitive)) == true
          end
        end

        describe "datatype?/1" do
          test "with itself" do
            assert unquote(datatype).datatype?(unquote(datatype)) == true
          end

          test "with non-RDF values" do
            assert unquote(datatype).datatype?(self()) == false
            assert unquote(datatype).datatype?(Elixir.Enum) == false
            assert unquote(datatype).datatype?(:foo) == false
          end

          unless unquote(primitive) do
            test "on a base datatype" do
              # We're using apply here to suppress "nil.datatype?/1 is undefined" warnings caused by the primitives
              assert apply(unquote(base), :datatype?, [unquote(datatype)]) == true
              assert apply(unquote(base_primitive), :datatype?, [unquote(datatype)]) == true
            end
          end
        end

        test "applicable_facets/0" do
          assert MapSet.new(unquote(datatype).applicable_facets()) ==
                   MapSet.new(unquote(applicable_facets))
        end

        if unquote(facets) do
          test "facets" do
            Enum.each(unquote(facets), fn {facet, value} ->
              assert apply(unquote(datatype), facet, []) == value
            end)
          end
        end

        test "name/0" do
          assert unquote(datatype).name() == unquote(datatype_name)
        end

        test "id/0" do
          assert unquote(datatype).id() == RDF.iri(unquote(datatype_iri))
        end

        describe "general datatype?/1" do
          test "on the exact same datatype" do
            assert unquote(datatype).datatype?(unquote(datatype)) == true

            Enum.each(@valid, fn {input, _} ->
              literal = unquote(datatype).new(input)
              assert unquote(datatype).datatype?(literal) == true
              assert unquote(datatype).datatype?(literal.literal) == true
            end)
          end

          unless unquote(primitive) do
            test "on the base datatype" do
              assert unquote(base).datatype?(unquote(datatype)) == true

              Enum.each(@valid, fn {input, _} ->
                literal = unquote(datatype).new(input)
                assert unquote(base).datatype?(literal) == true
                assert unquote(base).datatype?(literal.literal) == true
              end)
            end

            test "on the base primitive datatype" do
              assert unquote(base_primitive).datatype?(unquote(datatype)) == true

              Enum.each(@valid, fn {input, _} ->
                literal = unquote(datatype).new(input)
                assert unquote(base_primitive).datatype?(literal) == true
                assert unquote(base_primitive).datatype?(literal.literal) == true
              end)
            end
          end
        end

        test "datatype_id/1" do
          Enum.each(@valid, fn {input, _} ->
            assert unquote(datatype).new(input) |> unquote(datatype).datatype_id() ==
                     RDF.iri(unquote(datatype_iri))
          end)
        end

        test "language/1" do
          Enum.each(@valid, fn {input, _} ->
            assert unquote(datatype).new(input) |> unquote(datatype).language() == nil
          end)
        end

        describe "general new" do
          Enum.each(@valid, fn {input, {value, lexical, _}} ->
            expected = %RDF.Literal{
              literal: %unquote(datatype){value: value, uncanonical_lexical: lexical}
            }

            @tag example: %{input: input, output: expected}
            test "valid: #{unquote(datatype)}.new(#{inspect(input)})", %{example: example} do
              assert unquote(datatype).new(example.input) == example.output
            end
          end)

          Enum.each(@invalid, fn value ->
            expected = %RDF.Literal{
              literal: %unquote(datatype){
                uncanonical_lexical: unquote(datatype).init_invalid_lexical(value, [])
              }
            }

            @tag example: %{input: value, output: expected}
            test "invalid: #{unquote(datatype)}.new(#{inspect(value)})",
                 %{example: example} do
              assert unquote(datatype).new(example.input) == example.output
            end
          end)

          test "canonicalize option" do
            Enum.each(@valid, fn {input, _} ->
              assert unquote(datatype).new(input, canonicalize: true) ==
                       unquote(datatype).new(input) |> unquote(datatype).canonical()
            end)

            Enum.each(@invalid, fn input ->
              assert unquote(datatype).new(input, canonicalize: true) ==
                       unquote(datatype).new(input) |> unquote(datatype).canonical()
            end)
          end
        end

        describe "general new!" do
          test "with valid values, it behaves the same as new" do
            Enum.each(@valid, fn {input, _} ->
              assert unquote(datatype).new!(input) == unquote(datatype).new(input)

              assert unquote(datatype).new!(input) ==
                       unquote(datatype).new(input)

              assert unquote(datatype).new!(input, canonicalize: true) ==
                       unquote(datatype).new(input, canonicalize: true)
            end)
          end

          test "with invalid values, it raises an error" do
            Enum.each(@invalid, fn value ->
              assert_raise ArgumentError, fn -> unquote(datatype).new!(value) end

              assert_raise ArgumentError, fn ->
                unquote(datatype).new!(value, canonicalize: true)
              end
            end)
          end
        end

        describe "general value" do
          Enum.each(@valid, fn {input, {value, _, canonicalized}} ->
            @tag example: %{input: input, value: value}
            test "of valid #{unquote(datatype)}.new(#{inspect(input)})",
                 %{example: example} do
              assert unquote(datatype).new(example.input) |> unquote(datatype).value() ==
                       example.value
            end
          end)

          Enum.each(@invalid, fn value ->
            @tag example: %{input: value, value: value}
            test "of invalid #{unquote(datatype)}.new(#{inspect(value)})", %{example: example} do
              assert unquote(datatype).new(example.input) |> unquote(datatype).value() == nil
            end
          end)
        end

        describe "general lexical" do
          Enum.each(@valid, fn {input, {_, lexical, canonicalized}} ->
            lexical = lexical || canonicalized
            @tag example: %{input: input, lexical: lexical}
            test "of valid #{unquote(datatype)}.new(#{inspect(input)})",
                 %{example: example} do
              assert unquote(datatype).new(example.input) |> unquote(datatype).lexical() ==
                       example.lexical
            end
          end)

          Enum.each(@invalid, fn value ->
            lexical = unquote(datatype).init_invalid_lexical(value, [])
            @tag example: %{input: value, lexical: lexical}
            test "of invalid #{unquote(datatype)}.new(#{inspect(value)}) == #{inspect(lexical)}",
                 %{example: example} do
              assert unquote(datatype).new(example.input) |> unquote(datatype).lexical() ==
                       example.lexical
            end
          end)
        end

        describe "general canonicalization" do
          Enum.each(@valid, fn {input, {value, _, _}} ->
            expected = %RDF.Literal{literal: %unquote(datatype){value: value}}
            @tag example: %{input: input, output: expected}
            test "#{unquote(datatype)} #{inspect(input)}", %{example: example} do
              assert unquote(datatype).new(example.input) |> unquote(datatype).canonical() ==
                       example.output
            end
          end)

          Enum.each(@valid, fn {input, {_, _, canonicalized}} ->
            @tag example: %{input: input, canonicalized: canonicalized}
            test "lexical of canonicalized #{unquote(datatype)} #{inspect(input, limit: 4)} is #{inspect(canonicalized, limit: 4)}",
                 %{example: example} do
              assert unquote(datatype).new(example.input)
                     |> unquote(datatype).canonical()
                     |> unquote(datatype).lexical() ==
                       example.canonicalized
            end
          end)

          Enum.each(@valid, fn {input, {_, _, canonicalized}} ->
            @tag example: %{input: input, canonicalized: canonicalized}
            test "canonical? for #{unquote(datatype)} #{inspect(input)}", %{example: example} do
              literal = unquote(datatype).new(example.input)

              assert unquote(datatype).canonical?(literal) ==
                       (unquote(datatype).lexical(literal) == example.canonicalized)
            end
          end)

          test "does not change the XSD datatype value when it is invalid" do
            Enum.each(@invalid, fn value ->
              assert unquote(datatype).new(value) |> unquote(datatype).canonical() ==
                       unquote(datatype).new(value)
            end)
          end

          test "canonical_lexical with valid literals" do
            Enum.each(@valid, fn {input, {_, _, canonicalized}} ->
              assert unquote(datatype).new(input) |> unquote(datatype).canonical_lexical() ==
                       canonicalized
            end)
          end

          test "canonical_lexical with invalid literals" do
            Enum.each(@invalid, fn value ->
              assert unquote(datatype).new(value) |> unquote(datatype).canonical_lexical() ==
                       nil
            end)
          end
        end

        describe "general validation" do
          Enum.each(Map.keys(@valid), fn value ->
            @tag value: value
            test "#{inspect(value)} as a #{unquote(datatype)} is valid", %{value: value} do
              assert unquote(datatype).valid?(unquote(datatype).new(value))
            end
          end)

          Enum.each(@invalid, fn value ->
            @tag value: value
            test "#{inspect(value)} as a #{unquote(datatype)} is invalid", %{value: value} do
              refute unquote(datatype).valid?(unquote(datatype).new(value))
            end
          end)
        end
      end

      test "String.Chars protocol implementation" do
        Enum.each(@valid, fn {input, _} ->
          assert unquote(datatype).new(input) |> to_string() ==
                   unquote(datatype).new(input) |> unquote(datatype).lexical()
        end)
      end
    end
  end

  def dt(value) do
    {:ok, date, _} = DateTime.from_iso8601(value)
    date
  end
end
