# frozen_string_literal: true

require "dry/view/scope_builder"
require "dry/view/render_environment_missing"

RSpec::Matchers.define :scope do |locals|
  match do |actual|
    locals == actual._locals
  end
end

RSpec.describe Dry::View::Part do
  let(:name) { :user }
  let(:value) { double(:value) }
  let(:render_env) {
    Dry::View::RenderEnvironment.new(
      renderer: renderer,
      inflector: Dry::Inflector.new,
      context: Dry::View::Context.new,
      scope_builder: Dry::View::ScopeBuilder.new,
      part_builder: Dry::View::ScopeBuilder.new
    )
  }
  let(:renderer) { spy(:renderer, format: :xml) }

  context "with a render environment" do
    subject(:part) {
      described_class.new(
        name: name,
        value: value,
        render_env: render_env
      )
    }

    describe "#render" do
      it "renders a partial with the part available in its scope" do
        part.render(:info)
        expect(renderer).to have_received(:partial).with(:info, scope(user: part))
      end

      it "allows the part to be made available on a different name" do
        part.render(:info, as: :admin)
        expect(renderer).to have_received(:partial).with(:info, scope(admin: part))
      end

      it "includes extra locals in the scope" do
        part.render(:info, extra_local: "hello")
        expect(renderer).to have_received(:partial).with(:info, scope(user: part, extra_local: "hello"))
      end
    end

    describe "#to_s" do
      before do
        allow(value).to receive(:to_s).and_return "to_s on the value"
      end

      it "delegates to the wrapped value" do
        expect(part.to_s).to eq "to_s on the value"
      end
    end

    describe "#new" do
      it "preserves render environment" do
        new_part = part.new(value: "new value")
        expect(new_part._render_env).to be part._render_env
      end
    end

    describe "#inspect" do
      it "includes the clsas name, name, and value only" do
        expect(part.inspect).to eq "#<Dry::View::Part name=:user value=#<Double :value>>"
      end
    end

    describe "#_format" do
      it "returns the render environment's format" do
        expect(part._format).to eq :xml
      end
    end

    describe "#method_missing" do
      let(:value) { double(greeting: "hello from value") }

      it "calls a matching method on the value" do
        expect(part.greeting).to eq "hello from value"
      end

      it "forwards all arguments to the method" do
        blk = -> {}
        part.greeting "args", &blk

        expect(value).to have_received(:greeting).with("args", &blk)
      end

      it "raises an error if no method matches" do
        expect { part.farewell }.to raise_error(NoMethodError)
      end
    end

    describe "#respond_to?" do
      let(:value) { double(greeting: "hello from value") }

      it "handles convenience methods" do
        expect(part).to respond_to(:format)
        expect(part).to respond_to(:context)
        expect(part).to respond_to(:render)
        expect(part).to respond_to(:scope)
        expect(part).to respond_to(:value)
      end

      it "handles value methods" do
        expect(part).to respond_to(:greeting)
      end
    end
  end

  context "without a render environment" do
    subject(:part) {
      described_class.new(
        name: name,
        value: value
      )
    }

    describe "#format" do
      it "raises an error" do
        expect { part.render(:info) }.to raise_error(Dry::View::RenderEnvironmentMissing::MissingEnvironmentError)
      end
    end

    describe "#render" do
      it "raises an error" do
        expect { part.render(:info) }.to raise_error(Dry::View::RenderEnvironmentMissing::MissingEnvironmentError)
      end
    end

    describe "#scope" do
      it "raises an error" do
        expect { part.scope(:info) }.to raise_error(Dry::View::RenderEnvironmentMissing::MissingEnvironmentError)
      end
    end
  end

  context "without a name provided" do
    describe "#_name" do
      context "when class has a name" do
        before do
          Test::MyPart = Class.new(Dry::View::Part)
        end

        subject(:part) {
          Test::MyPart.new(value: value)
        }

        it "is inferred from the class name" do
          expect(part._name).to eq "my_part"
        end
      end

      context "when class is anonymous" do
        subject(:part) {
          Class.new(Dry::View::Part).new(value: value)
        }

        it "defaults to 'part'" do
          expect(part._name).to eq "part"
        end
      end
    end
  end
end
