# frozen_string_literal: true

require "spec_helper"

describe HashValidator do
  describe "Adding validators" do
    let(:new_validator1) { HashValidator::Validator::SimpleValidator.new("my_type1", lambda { |v| true }) }
    let(:new_validator2) { HashValidator::Validator::SimpleValidator.new("my_type2", lambda { |v| true }) }

    it "allows validators with unique names" do
      expect {
        HashValidator.add_validator(new_validator1)
      }.to_not raise_error
    end

    it "does not allow validators with conflicting names" do
      expect {
        HashValidator.add_validator(new_validator2)
        HashValidator.add_validator(new_validator2)
      }.to raise_error(StandardError, "validators need to have unique names")
    end

    it "does not allow validators that do not inherit from the base validator class" do
      expect {
        HashValidator.add_validator("Not a validator")
      }.to raise_error(StandardError, "validators need to inherit from HashValidator::Validator::Base")
    end

    it "raises ArgumentError when options hash is missing :pattern or :func key" do
      expect {
        HashValidator.add_validator("test_validator", { invalid_key: "value" })
      }.to raise_error(ArgumentError, "Options hash must contain either :pattern or :func key")
    end

    it "raises ArgumentError when second argument is not a hash" do
      expect {
        HashValidator.add_validator("test_validator", "not_a_hash")
      }.to raise_error(ArgumentError, "Second argument must be an options hash with :pattern or :func key")
    end

    it "raises ArgumentError when wrong number of arguments are provided" do
      expect {
        HashValidator.add_validator("test", "arg2", "arg3")
      }.to raise_error(ArgumentError, "add_validator expects 1 argument (validator instance) or 2 arguments (name, options)")
    end
  end

  describe "#validate" do
    describe "individual type validations" do
      it "should validate hash" do
        expect(validate({ v: {} }, { v: {} }).valid?).to eq true

        expect(validate({ v: "" }, { v: {} }).valid?).to eq false
        expect(validate({ v: "" }, { v: {} }).errors).to eq({ v: "hash required" })
      end

      it "should validate presence" do
        expect(validate({ v: "test" }, { v: "required" }).valid?).to eq true
        expect(validate({ v: 1234   }, { v: "required" }).valid?).to eq true

        expect(validate({ v: nil    }, { v: "required" }).valid?).to eq false
        expect(validate({ v: nil    }, { v: "required" }).errors).to eq({ v: "is required" })

        expect(validate({ x: "test" }, { v: "required" }).valid?).to eq false
        expect(validate({ x: "test" }, { v: "required" }).errors).to eq({ v: "is required" })

        expect(validate({ x: 1234   }, { v: "required" }).valid?).to eq false
        expect(validate({ x: 1234   }, { v: "required" }).errors).to eq({ v: "is required" })
      end

      it "should validate string" do
        expect(validate({ v: "test" }, { v: "string" }).valid?).to eq true

        expect(validate({ v: 123456 }, { v: "string" }).valid?).to eq false
        expect(validate({ v: 123456 }, { v: "string" }).errors).to eq({ v: "string required" })
      end

      it "should validate numeric" do
        expect(validate({ v: 1234 }, { v: "numeric" }).valid?).to eq true
        expect(validate({ v: "12" }, { v: "numeric" }).valid?).to eq false
      end

      it "should validate array" do
        expect(validate({ v: [ 1, 2, 3 ] }, { v: "array" }).valid?).to eq true
        expect(validate({ v: " 1,2,3 " }, { v: "array" }).valid?).to eq false
      end

      it "should validate time" do
        expect(validate({ v: Time.now                    }, { v: "time" }).valid?).to eq true
        expect(validate({ v: "2013-04-12 13:18:05 +0930" }, { v: "time" }).valid?).to eq false
      end
    end

    describe "validator syntax" do
      it "should allow strings as validator names" do
        expect(validate({ v: "test" }, { v: "string" }).valid?).to eq true
      end

      it "should allow symbols as validator names" do
        expect(validate({ v: "test" }, { v: :string }).valid?).to eq true
      end
    end

    describe "full validations" do
      let(:empty_hash) { {} }

      let(:simple_hash) { {
          foo: 1,
          bar: "baz"
        }}

      let(:invalid_simple_hash) { {
          foo: 1,
          bar: 2
        }}

      let(:complex_hash) { {
          foo: 1,
          bar: "baz",
          user: {
            first_name: "James",
            last_name:  "Brooks",
            age:        27,
            likes:      [ "Ruby", "Kendo", "Board Games" ]
          }
        }}

      let(:invalid_complex_hash) { {
          foo: 1,
          bar: 2,
          user: {
            first_name: "James",
            last_name:  "Brooks",
            likes:      "Ruby, Kendo, Board Games"
          }
        }}

      describe "no validations" do
        let(:validations) { {} }

        it "should validate an empty hash" do
          v = validate(empty_hash, validations)
          expect(v.valid?).to eq true
          expect(v.errors).to be_empty
        end

        it "should validate a simple hash" do
          v = validate(simple_hash, validations)
          expect(v.valid?).to eq true
          expect(v.errors).to be_empty
        end

        it "should validate a simple hash 2" do
          v = validate(invalid_simple_hash, validations)
          expect(v.valid?).to eq true
          expect(v.errors).to be_empty
        end

        it "should validate a complex hash" do
          v = validate(complex_hash, validations)
          expect(v.valid?).to eq true
          expect(v.errors).to be_empty
        end

        it "should validate a complex hash 2" do
          v = validate(invalid_complex_hash, validations)
          expect(v.valid?).to eq true
          expect(v.errors).to be_empty
        end
      end

      describe "simple validations" do
        let(:validations) { { foo: "numeric", bar: "string" } }

        it "should not validate an empty hash (stating missing with required)" do
          v = validate(empty_hash, validations)
          expect(v.valid?).to eq false
          expect(v.errors).to eq({ foo: "numeric required", bar: "string required" })
        end

        it "should validate a simple hash" do
          v = validate(simple_hash, validations)
          expect(v.valid?).to eq true
          expect(v.errors).to be_empty
        end

        it "should not validate a simple hash 2" do
          v = validate(invalid_simple_hash, validations)
          expect(v.valid?).to eq false
          expect(v.errors).to eq({ bar: "string required" })
        end

        it "should validate a complex hash" do
          v = validate(complex_hash, validations)
          expect(v.valid?).to eq true
          expect(v.errors).to be_empty
        end

        it "should not validate a complex hash 2" do
          v = validate(invalid_complex_hash, validations)
          expect(v.valid?).to eq false
          expect(v.errors).to eq({ bar: "string required" })
        end
      end

      describe "nested validations" do
        let(:validations) { {
          foo: "numeric",
          bar: "string",
          user: {
            first_name: "string",
            last_name:  /^(Brooks|Smith)$/,
            age:        "required",
            likes:      "array"
          }
        }}

        it "should validate a complex hash" do
          v = validate(complex_hash, validations)
          expect(v.valid?).to eq true
          expect(v.errors).to be_empty
        end

        it "should not validate a complex hash 2" do
          v = validate(invalid_complex_hash, validations)
          expect(v.valid?).to eq false
          expect(v.errors).to eq({ bar: "string required", user: { age: "is required", likes: "array required" } })
        end
      end

      describe "optional validations" do
        let(:validations) { { foo: "numeric", bar: HashValidator.optional("string") } }

        it "should validate a complex hash" do
          v = validate(complex_hash, validations)
          expect(v.valid?).to eq true
          expect(v.errors).to be_empty
        end

        it "should not validate a complex hash 2" do
          v = validate(invalid_complex_hash, validations)
          expect(v.valid?).to eq false
          expect(v.errors).to eq({ bar: "string required" })
        end
      end

      describe "many validations" do
        let(:validations) { { foo: "numeric", bar: "string", user: { first_name: "string", likes: HashValidator.many("string") } } }

        it "should validate a complex hash" do
          v = validate(complex_hash, validations)
          expect(v.valid?).to eq true
          expect(v.errors).to be_empty
        end

        it "should not validate a complex hash 2" do
          v = validate(invalid_complex_hash, validations)
          expect(v.valid?).to eq false
          expect(v.errors).to eq({ bar: "string required", user: { likes: "enumerable required" } })
        end
      end

      describe "multiple validations" do
        let(:validations) { { foo: "numeric", user: { age: HashValidator.multiple("numeric", 1..100) } } }

        it "should validate a complex hash" do
          v = validate(complex_hash, validations)
          expect(v.valid?).to eq true
          expect(v.errors).to be_empty
        end

        it "should not validate a complex hash 2" do
          v = validate(invalid_complex_hash, validations)
          expect(v.valid?).to eq false
          expect(v.errors).to eq({ user: { age: "numeric required, value from list required" } })
        end
      end
    end
  end
end

describe "Strict Validation" do
  let(:simple_hash) { { foo: "bar", bar: "foo" } }

  let(:complex_hash) { {
      foo: 1,
      user: {
        first_name: "James",
        last_name:  "Brooks",
        age:        27,
        likes:      [ "Ruby", "Kendo", "Board Games" ]
      }
    }}

  let(:validations) { { foo: "string" } }

  let(:complex_validations) { {
      foo: "integer",
      user: {
        first_name: "string", age: "integer"
      }
    }}

  it "reports which keys are not expected for a simple hash" do
    v = validate(simple_hash, validations, true)
    expect(v.valid?).to eq false
    expect(v.errors).to eq({ bar: "key not expected" })
  end

  it "reports which keys are not expected for a complex hash" do
    v = validate(complex_hash, complex_validations, true)
    expect(v.valid?).to eq false
    expect(v.errors).to eq(user: { last_name: "key not expected", likes: "key not expected" })
  end
end
