# frozen_string_literal: true

RSpec.describe Dry::Types::Sum do
  describe "common nominal behavior" do
    subject(:type) { Dry::Types["bool"] }

    it_behaves_like "Dry::Types::Nominal#meta"
    it_behaves_like "Dry::Types::Nominal without primitive"
    it_behaves_like "a composable constructor"

    it "is frozen" do
      expect(type).to be_frozen
    end
  end

  it_behaves_like "a constrained type" do
    let(:type) { Dry::Types["integer"] | Dry::Types["string"] }

    it_behaves_like "a composable constructor"
  end

  describe "#optional?" do
    it "return true if left side is nil" do
      type = Dry::Types["strict.nil"] | Dry::Types["nominal.string"]

      expect(type).to be_optional
    end

    it "return true if right side is nil" do
      type = Dry::Types["nominal.string"] | Dry::Types["nominal.nil"]

      expect(type).to be_optional
    end

    it "works when left is a Sum type" do
      type = Dry::Types["strict.integer"] | Dry::Types["strict.date"] | Dry::Types["strict.string"]

      expect(type).to_not be_optional
    end
  end

  describe "#[]" do
    it "works with two pass-through types" do
      type = Dry::Types["nominal.integer"] | Dry::Types["nominal.string"]

      expect(type[312]).to be(312)
      expect(type["312"]).to eql("312")
      expect(type[nil]).to be(nil)
    end

    it "works with two strict types" do
      type = Dry::Types["integer"] | Dry::Types["string"]

      expect(type[312]).to be(312)
      expect(type["312"]).to eql("312")

      expect { type[{}] }.to raise_error(Dry::Types::CoercionError)
    end

    it "works with nil and strict types" do
      type = Dry::Types["strict.nil"] | Dry::Types["strict.string"]

      expect(type[nil]).to be(nil)
      expect(type["312"]).to eql("312")

      expect { type[{}] }.to raise_error(Dry::Types::CoercionError)
    end

    it "is aliased as #call" do
      type = Dry::Types["nominal.integer"] | Dry::Types["nominal.string"]
      expect(type.call(312)).to be(312)
      expect(type.call("312")).to eql("312")
    end

    it "works with two constructor & constrained types" do
      left = Dry::Types["array<string>"]
      right = Dry::Types["array<hash>"]

      type = left | right

      expect(type[%w[foo bar]]).to eql(%w[foo bar])

      expect(type[[{name: "foo"}, {name: "bar"}]]).to eql([
        {name: "foo"}, {name: "bar"}
      ])
    end

    it "works with two complex types with constraints" do
      pair = Dry::Types["array"]
        .of(Dry::Types["coercible.string"])
        .constrained(size: 2)

      string_list = Dry::Types["array"]
        .of(Dry::Types["string"])
        .constrained(min_size: 1)

      string_pairs = Dry::Types["array"]
        .of(pair)
        .constrained(min_size: 1)

      type = string_list | string_pairs

      expect(type.(%w[foo])).to eql(%w[foo])
      expect(type.(%w[foo bar])).to eql(%w[foo bar])

      expect(type.([[1, "foo"], [2, "bar"]])).to eql([%w[1 foo], %w[2 bar]])

      expect { type[:oops] }.to raise_error(Dry::Types::ConstraintError, /:oops/)

      expect { type[[]] }.to raise_error(Dry::Types::ConstraintError, /\[\]/)

      expect { type.([%i[foo]]) }.to raise_error(Dry::Types::ConstraintError, /\[:foo\]/)

      expect { type.([[1], [2]]) }.to raise_error(Dry::Types::ConstraintError, /2, \[1\]/)
    end
  end

  describe "#try" do
    subject(:type) { Dry::Types["strict.bool"] }

    it "returns success when value passed" do
      expect(type.try(true)).to be_success
    end

    it "returns failure when value did not pass" do
      expect(type.try("true")).to be_failure
    end
  end

  describe "#success" do
    subject(:type) { Dry::Types["strict.bool"] }

    it "returns success when value passed" do
      expect(type.success(true)).to be_success
    end

    it "raises ArgumentError when non of the types have a valid input" do
      expect {
        type.success("true")
      }.to raise_error(ArgumentError, /Invalid success value 'true'/)
    end
  end

  describe "#failure" do
    subject(:type) { Dry::Types["integer"] | Dry::Types["string"] }

    it "returns failure when invalid value is passed" do
      expect(type.failure(true)).to be_failure
    end
  end

  describe "#===" do
    subject(:type) { Dry::Types["integer"] | Dry::Types["string"] }

    it "returns boolean" do
      expect(type.===("hello")).to eql(true)
      expect(type.nil?).to eql(false)
    end

    context "in case statement" do
      let(:value) do
        case "world"
        when type then "accepted"
        else "invalid"
        end
      end

      it "returns correct value" do
        expect(value).to eql("accepted")
      end
    end
  end

  describe "#default" do
    it "returns a default value sum type" do
      type = (Dry::Types["nominal.nil"] | Dry::Types["nominal.string"]).default("foo")

      expect(type.call).to eql("foo")
    end

    it "supports a sum type which includes a constructor type" do
      type = (Dry::Types["params.nil"] | Dry::Types["params.integer"]).default(3)
      expect(type[""]).to be(nil)
    end

    it "supports a sum type which includes a constrained constructor type" do
      type = (Dry::Types["strict.nil"] | Dry::Types["coercible.integer"]).default(3)

      expect(type[]).to be(3)
      expect(type["3"]).to be(3)
      expect(type["7"]).to be(7)
    end
  end

  describe "#constructor" do
    let(:type) { (Dry::Types["nominal.string"] | Dry::Types["nominal.nil"]).constructor { |input| input ? "#{input} world" : input } }

    it "returns the correct value" do
      expect(type.call("hello")).to eql("hello world")
      expect(type.call(nil)).to eql(nil)
      expect(type.call(10)).to eql("10 world")
    end

    it "returns if value is valid" do
      expect(type.valid?("hello")).to eql(true)
      expect(type.valid?(nil)).to eql(true)
      expect(type.valid?(10)).to eql(true)
    end

    it "works with sum of complex constructors" do
      a = Dry::Types["string"].constrained(size: 2) | Dry::Types["hash"]
      b = Dry::Types["string"].constrained(size: 1) | Dry::Types["hash"]

      c = a.constructor { |x| x.is_a?(Hash) ? x : x.downcase } |
          b.constructor { |x| x.is_a?(Hash) ? x : x.upcase }

      expect(c.call({})).to eq({})
      expect(c.call("A")).to eq("A")
      expect(c.call("aa")).to eq("aa")

      expect { c.call("aaa") }.to raise_error(Dry::Types::ConstraintError)
    end
  end

  describe "#rule" do
    let(:two_addends) { Dry::Types["strict.nil"] | Dry::Types["strict.string"] }

    shared_examples_for "a disjunction of constraints" do
      it "returns a rule" do
        rule = type.rule

        expect(rule.(nil)).to be_success
        expect(rule.("1")).to be_success
        expect(rule.(1)).to be_failure
      end
    end

    it_behaves_like "a disjunction of constraints" do
      subject(:type) { two_addends }
    end

    it_behaves_like "a disjunction of constraints" do
      subject(:type) { Dry::Types["strict.true"] | two_addends }

      it "accepts true" do
        rule = type.rule

        expect(rule.(true)).to be_success
        expect(rule.(false)).to be_failure
      end
    end

    it_behaves_like "a disjunction of constraints" do
      subject(:type) { two_addends | Dry::Types["strict.true"] }

      it "accepts true" do
        rule = type.rule

        expect(rule.(true)).to be_success
        expect(rule.(false)).to be_failure
      end
    end
  end

  describe "#to_s" do
    context "shallow sum" do
      let(:type) { Dry::Types["nominal.string"] | Dry::Types["nominal.integer"] }

      it "returns string representation of the type" do
        expect(type.to_s).to eql("#<Dry::Types[Sum<Nominal<String> | Nominal<Integer>>]>")
      end
    end

    context "constrained" do
      let(:type) do
        Dry::Types["nominal.string"].constrained(format: /foo/) |
          Dry::Types["nominal.string"].constrained(min_size: 4)
      end

      it "returns string representation of the type" do
        expect(type.to_s).to eql(
          "#<Dry::Types[Sum<" \
          "Constrained<Nominal<String> rule=[format?(/foo/)]> | " \
          "Constrained<Nominal<String> rule=[min_size?(4)]>>]>"
        )
      end
    end

    context "sum tree" do
      let(:type) do
        Dry::Types["nominal.string"] | Dry::Types["nominal.integer"] |
          (Dry::Types["nominal.date"] | Dry::Types["nominal.time"])
      end

      it "returns string representation of the type" do
        expect(type.to_s).to eql(
          "#<Dry::Types[Sum<" \
          "Nominal<String> | " \
          "Nominal<Integer> | " \
          "Nominal<Date> | " \
          "Nominal<Time>" \
          ">]>"
        )
      end
    end

    context "with dry-struct types" do
      before { require "dry-struct" }

      let(:struct_a) do
        Class.new(Dry::Struct) do
          def self.name
            "TestStructA"
          end
        end
      end

      let(:struct_b) do
        Class.new(Dry::Struct) do
          def self.name
            "TestStructB"
          end
        end
      end

      it "works with sum of two structs" do
        sum_type = struct_a | struct_b
        expect { sum_type.to_s }.not_to raise_error
      end
    end
  end

  context "with map type" do
    let(:map_type) do
      Dry::Types["hash"].map(Dry::Types["symbol"], Dry::Types["string"])
    end

    let(:string_type) { Dry::Types["string"] }

    subject(:type) { map_type | string_type }

    it "rejects invalid input" do
      expect(type.valid?(12_345)).to be false
      expect { type[12_345] }.to raise_error(Dry::Types::ConstraintError)
    end
  end

  describe "#meta" do
    context "optional types" do
      let(:meta) { {foo: :bar} }

      subject(:type) { Dry::Types["string"].optional }

      it "uses meta from the right branch" do
        expect(type.meta(meta).meta).to eql(meta)
        expect(type.meta(meta).right.meta).to eql(meta)
      end
    end
  end
end
