RSpec.describe Evil::Client::Model do
  before { class Test::Model < described_class; end }

  let(:model)    { klass.new(**options) }
  let(:klass)    { Test::Model }
  let(:options)  { { id: 42, name: "Andrew" } }
  let(:dsl_methods) do
    %i[options datetime logger scope basic_auth key_auth token_auth]
  end

  describe ".policy" do
    subject { klass.policy }

    it "subclasses Evil::Client::Policy" do
      expect(subject.superclass).to eq described_class.policy
      expect(described_class.policy.superclass).to eq Tram::Policy
    end

    it "refers back to the model" do
      expect(subject.model).to eq klass
    end
  end

  describe ".option" do
    it "is defined by Dry::Initializer DSL" do
      expect(klass).to be_a Dry::Initializer
    end

    it "fails when method name is reserved for DSL" do
      dsl_methods.each do |name|
        expect { klass.option name }
          .to raise_error Evil::Client::NameError
      end
    end

    it "allows the option to be renamed" do
      expect { klass.option :basic_auth, as: :something }.not_to raise_error
    end
  end

  describe ".let" do
    before do
      klass.option :id
      klass.let(:square_id) { id**2 }
    end

    subject { model.square_id }

    it "adds the corresponding memoizer to the instance" do
      expect(subject).to eq(42**2)
    end

    it "fails when method name is reserved for DSL" do
      dsl_methods.each do |name|
        expect { klass.let(name) { 0 } }
          .to raise_error Evil::Client::NameError
      end
    end
  end

  describe ".validate" do
    before do
      klass.option :name
      klass.validate { errors.add :name_present if name.to_s == "" }
    end

    let(:options) { { name: "" } }

    it "adds validation for an instance" do
      # see spec/fixtures/locale/en.yml
      expect { model }
        .to raise_error(StandardError, /The user has no name/)
    end
  end

  describe ".new" do
    subject { model }

    context "with wrong options" do
      before { klass.option :user, as: :customer }

      it "raises StandardError" do
        expect { subject }.to raise_error StandardError, /user/
      end
    end
  end

  describe ".extend" do
    before do
      class Test::Other < described_class
        option :first_name, optional: true
        option :last_name,  optional: true

        let(:name) { [first_name, last_name].compact.join(" ") }

        validate { errors.add :empty_name if name == "" }
      end

      class Test::Model < described_class
        extend Test::Other
        option :email, optional: true
      end
    end

    let(:options) do
      { first_name: "Joe", last_name: "Doe", email: "joe@example.com" }
    end

    subject { model }

    context "with kwargs" do
      let(:model) { klass.new(**options) }

      it "behaves like a model" do
        expect(subject).to be_a klass
        expect(subject.email).to eq "joe@example.com"
      end

      it "injects options from the other model" do
        expect(subject.first_name).to eq "Joe"
        expect(subject.last_name).to  eq "Doe"
      end

      it "injects memoizers from the other model" do
        expect(subject.name).to eq "Joe Doe"
      end
    end

    context "with hash argument" do
      let(:model) { klass.new(options) }

      it "behaves like a model" do
        expect(subject).to be_a klass
        expect(subject.email).to eq "joe@example.com"
      end

      it "injects options from the other model" do
        expect(subject.first_name).to eq "Joe"
        expect(subject.last_name).to  eq "Doe"
      end

      it "injects memoizers from the other model" do
        expect(subject.name).to eq "Joe Doe"
      end
    end

    context "with invalid options" do
      let(:options) { { email: "joe@example.com" } }

      it "injects validators from the other model" do
        expect { subject }.to raise_error(StandardError, /name/)
      end
    end
  end
end
