describe "a generated stub instance" do
  include FactoryBot::Syntax::Methods

  before do
    define_model("User")

    define_model("Post", title: :string,
                         body: :string,
                         age: :integer,
                         user_id: :integer,
                         draft: :boolean) do
      belongs_to :user
    end

    FactoryBot.define do
      factory :user

      factory :post do
        title { "default title" }
        body { "default body" }
        user
      end
    end
  end

  subject { build_stubbed(:post, title: "overridden title") }

  it "assigns a default attribute" do
    expect(subject.body).to eq "default body"
  end

  it "assigns an overridden attribute" do
    expect(subject.title).to eq "overridden title"
  end

  it "assigns associations" do
    expect(subject.user).to be_kind_of(User)
  end

  it "has an id" do
    expect(subject.id).to be > 0
  end

  it "generates unique ids" do
    other_stub = build_stubbed(:post)
    expect(subject.id).not_to eq other_stub.id
  end

  it "isn't a new record" do
    expect(subject).not_to be_new_record
  end

  it "assigns associations that aren't new records" do
    expect(subject.user).not_to be_new_record
  end

  it "isn't changed" do
    expect(subject).not_to be_changed
  end

  it "disables connection" do
    expect { subject.connection }.to raise_error(RuntimeError)
  end

  it "disables update_attribute" do
    expect { subject.update_attribute(:title, "value") }.to raise_error(RuntimeError)
  end

  it "disables reload" do
    expect { subject.reload }.to raise_error(RuntimeError)
  end

  it "disables destroy" do
    expect { subject.destroy }.to raise_error(RuntimeError)
  end

  it "disables save" do
    expect { subject.save }.to raise_error(RuntimeError)
  end

  it "disables increment!" do
    expect { subject.increment!(:age) }.to raise_error(RuntimeError)
  end

  it "disables decrement!" do
    expect { subject.decrement!(:age) }.to raise_error(RuntimeError)
  end

  it "disables toggle!" do
    expect { subject.toggle!(:draft) }.to raise_error(RuntimeError)
  end

  it "allows increment" do
    subject.age = 1
    subject.increment(:age)
    expect(subject.age).to eq(2)
  end

  it "allows decrement" do
    subject.age = 1
    subject.decrement(:age)
    expect(subject.age).to eq(0)
  end

  it "allows toggle" do
    subject.draft = true
    subject.toggle(:draft)
    expect(subject).not_to be_draft
  end
end

describe "calling `build_stubbed` with a block" do
  include FactoryBot::Syntax::Methods

  before do
    define_model("Company", name: :string)

    FactoryBot.define do
      factory :company
    end
  end

  it "passes the stub instance" do
    build_stubbed(:company, name: "thoughtbot") do |company|
      expect(company.name).to eq("thoughtbot")
      expect { company.save }.to raise_error(RuntimeError)
    end
  end

  it "returns the stub instance" do
    expected = nil
    result = build_stubbed(:company) { |company|
      expected = company
      "hello!"
    }
    expect(result).to eq expected
  end
end

describe "defaulting `created_at`" do
  include FactoryBot::Syntax::Methods

  before do
    define_model("ThingWithTimestamp", created_at: :datetime)
    define_model("ThingWithoutTimestamp")

    FactoryBot.define do
      factory :thing_with_timestamp
      factory :thing_without_timestamp
    end
  end

  it "defaults created_at for objects with created_at" do
    expect(build_stubbed(:thing_with_timestamp).created_at).to be_about_now
  end

  it "is doesn't mark the object as changed" do
    stub = build_stubbed(:thing_with_timestamp)
    expect(stub).not_to be_changed
  end

  it "doesn't add created_at to objects who don't have the method" do
    expect(build_stubbed(:thing_without_timestamp))
      .not_to respond_to(:created_at)
  end

  it "allows overriding created_at for objects with created_at" do
    created_at = 3.days.ago
    stubbed = build_stubbed(:thing_with_timestamp, created_at: created_at)
    expect(stubbed.created_at).to eq created_at
  end

  it "doesn't allow setting created_at on an object that doesn't define it" do
    expect { build_stubbed(:thing_without_timestamp, created_at: Time.now) }
      .to raise_error(NoMethodError, /created_at=/)
  end

  it "allows assignment of created_at" do
    stub = build_stubbed(:thing_with_timestamp)
    expect(stub.created_at).to be_about_now
    past_time = 3.days.ago
    stub.created_at = past_time
    expect(stub.created_at).to eq past_time
  end

  it "behaves the same as a non-stubbed created_at" do
    define_model("ThingWithCreatedAt", created_at: :datetime) do
      def created_at
        :the_real_created_at
      end
    end

    FactoryBot.define do
      factory :thing_with_created_at
    end

    stub = build_stubbed(:thing_with_created_at)
    persisted = create(:thing_with_created_at)

    expect(stub.created_at).to eq(persisted.created_at)
  end
end

describe "defaulting `updated_at`" do
  include FactoryBot::Syntax::Methods

  before do
    define_model("ThingWithTimestamp", updated_at: :datetime)
    define_model("ThingWithoutTimestamp")

    FactoryBot.define do
      factory :thing_with_timestamp
      factory :thing_without_timestamp
    end
  end

  it "defaults updated_at for objects with updated_at" do
    expect(build_stubbed(:thing_with_timestamp).updated_at).to be_about_now
  end

  it "is doesn't mark the object as changed" do
    stub = build_stubbed(:thing_with_timestamp)
    expect(stub).not_to be_changed
  end

  it "doesn't add updated_at to objects who don't have the method" do
    expect(build_stubbed(:thing_without_timestamp))
      .not_to respond_to(:updated_at)
  end

  it "allows overriding updated_at for objects with updated_at" do
    past_time = 3.days.ago
    stubbed = build_stubbed(:thing_with_timestamp, updated_at: past_time)
    expect(stubbed.updated_at).to eq past_time
  end

  it "doesn't allow setting updated_at on an object that doesn't define it" do
    expect {
      build_stubbed(:thing_without_timestamp, updated_at: Time.now)
    }.to raise_error(NoMethodError, /updated_at=/)
  end

  it "allows assignment of updated_at" do
    stub = build_stubbed(:thing_with_timestamp)
    expect(stub.updated_at).to be_about_now
    past_time = 3.days.ago
    stub.updated_at = past_time
    expect(stub.updated_at).to eq past_time
  end

  it "behaves the same as a non-stubbed updated_at" do
    define_model("ThingWithUpdatedAt", updated_at: :datetime) do
      def updated_at
        :the_real_updated_at
      end
    end

    FactoryBot.define do
      factory :thing_with_updated_at
    end

    stub = build_stubbed(:thing_with_updated_at)
    persisted = create(:thing_with_updated_at)

    expect(stub.updated_at).to eq(persisted.updated_at)
  end
end

describe "defaulting `id`" do
  before do
    define_model("Post")

    FactoryBot.define do
      factory :post
    end
  end

  it "allows overriding id" do
    expect(FactoryBot.build_stubbed(:post, id: 12).id).to eq 12
  end
end

describe "configuring the starting id" do
  it "defines which id build_stubbed instances start with" do
    define_model("Post")

    FactoryBot.define do
      factory :post
    end

    FactoryBot.build_stubbed_starting_id = 1000

    expect(FactoryBot.build_stubbed(:post).id).to eq 1000

    FactoryBot.build_stubbed_starting_id = 3000

    expect(FactoryBot.build_stubbed(:post).id).to eq 3000
  end
end
