require "test_helper"

class PopulatorTest < MiniTest::Spec
  Song  = Struct.new(:title, :album, :composer)
  Album = Struct.new(:name, :songs, :artist)
  Artist = Struct.new(:name)

  class AlbumForm < TestForm
    property :name, populator: ->(options) { self.name = options[:fragment].reverse }
    validation do
      params { required(:name).filled }
    end

    collection :songs,
               populator: ->(fragment:, model:, index:, **) {
                 (item = model[index]) ? item : model.insert(index, Song.new)
               } do
      property :title
      validation do
        params { required(:title).filled }
      end

      property :composer, populator: ->(options) { options[:model] || self.composer = Artist.new } do
        property :name
        validation do
          params { required(:name).filled }
        end
      end
    end

    # property :artist, populator: lambda { |fragment, options| (item = options.binding.get) ? item : Artist.new } do
    # NOTE: we have to document that model here is the twin!
    property :artist, populator: ->(options) { options[:model] || self.artist = Artist.new } do
      property :name
    end
  end

  let(:song)               { Song.new("Broken") }
  let(:song_with_composer) { Song.new("Resist Stance", nil, composer) }
  let(:composer)           { Artist.new("Greg Graffin") }
  let(:artist)             { Artist.new("Bad Religion") }
  let(:album)              { Album.new("The Dissent Of Man", [song, song_with_composer], artist) }

  let(:form) { AlbumForm.new(album) }

  it "runs populator on scalar" do
    form.validate(
      "name" => "override me!"
    )

    assert_equal form.name, "!em edirrevo"
  end

  # changing existing property :artist.
  # TODO: check with artist==nil
  it do
    old_id = artist.object_id

    form.validate(
      "artist" => {"name" => "Marcus Miller"}
    )

    assert_equal form.artist.model.object_id, old_id
  end

  # use populator for default value on scalars?

  # adding to collection via :populator.
  # valid.
  it "yyy" do
    assert form.validate(
      "songs"  => [{"title" => "Fallout"}, {"title" => "Roxanne"},
        {"title" => "Rime Of The Ancient Mariner"}, # new song.
        {"title" => "Re-Education", "composer" => {"name" => "Rise Against"}}], # new song with new composer.
    )

    assert_equal form.errors.messages.inspect, "{}"

    # form has updated.
    assert_equal form.name, "The Dissent Of Man"
    assert_equal form.songs[0].title, "Fallout"
    assert_equal form.songs[1].title, "Roxanne"
    assert_equal form.songs[1].composer.name, "Greg Graffin"

    form.songs[1].composer.model.is_a? Artist

    assert_equal form.songs[1].title, "Roxanne"
    assert_equal form.songs[2].title, "Rime Of The Ancient Mariner" # new song added.
    assert_equal form.songs[3].title, "Re-Education"
    assert_equal form.songs[3].composer.name, "Rise Against"
    assert_equal form.songs.size, 4
    assert_equal form.artist.name, "Bad Religion"

    # model has not changed, yet.
    assert_equal album.name, "The Dissent Of Man"
    assert_equal album.songs[0].title, "Broken"
    assert_equal album.songs[1].title, "Resist Stance"
    assert_equal album.songs[1].composer.name, "Greg Graffin"
    assert_equal album.songs.size, 2
    assert_equal album.artist.name, "Bad Religion"
  end
end

class PopulateWithMethodTest < Minitest::Spec
  Album = Struct.new(:title)

  class AlbumForm < TestForm
    property :title, populator: :title!

    def title!(options)
      self.title = options[:fragment].reverse
    end
  end

  let(:form) { AlbumForm.new(Album.new) }

  it "runs populator method" do
    form.validate("title" => "override me!")

    assert_equal form.title, "!em edirrevo"
  end
end

class PopulateWithCallableTest < Minitest::Spec
  Album = Struct.new(:title)

  class TitlePopulator
    include Uber::Callable

    def call(form:, **options)
      form.title = options[:fragment].reverse
    end
  end

  class TitlePopulatorWithOldSignature
    include Uber::Callable

    def call(form, options)
      form.title = options[:fragment].reverse
    end
  end

  class AlbumForm < TestForm
    property :title, populator: TitlePopulator.new
  end

  class AlbumFormWithOldPopulator < TestForm
    property :title, populator: TitlePopulatorWithOldSignature.new
  end

  let(:form) { AlbumForm.new(Album.new) }

  it "runs populator method" do
    form.validate("title" => "override me!")

    assert_equal form.title, "!em edirrevo"
  end

  it "gives warning when `form` is accepted as a positional argument" do
    _, warnings = capture_io do
      form = AlbumFormWithOldPopulator.new(Album.new)
      form.validate("title" => "override me!")

      assert_equal form.title, "!em edirrevo"
    end

    assert_equal warnings, %{[Reform] Accepting `form` as a positional argument in `:populator` will be deprecated. Please use `def call(form:, **options)` signature instead.
}
  end
end

class PopulateWithProcTest < Minitest::Spec
  Album = Struct.new(:title)

  TitlePopulator = ->(options) do
    options[:represented].title = options[:fragment].reverse
  end

  class AlbumForm < TestForm
    property :title, populator: TitlePopulator
  end

  let(:form) { AlbumForm.new(Album.new) }

  it "runs populator method" do
    form.validate("title" => "override me!")

    assert_equal form.title, "!em edirrevo"
  end
end

class PopulateIfEmptyTest < MiniTest::Spec
  Song  = Struct.new(:title, :album, :composer)
  Album = Struct.new(:name, :songs, :artist)
  Artist = Struct.new(:name)

  let(:song)               { Song.new("Broken") }
  let(:song_with_composer) { Song.new("Resist Stance", nil, composer) }
  let(:composer)           { Artist.new("Greg Graffin") }
  let(:artist)             { Artist.new("Bad Religion") }
  let(:album)              { Album.new("The Dissent Of Man", [song, song_with_composer], artist) }

  class AlbumForm < TestForm
    property :name

    collection :songs,
      populate_if_empty: Song do                                                # class name works.

      property :title
      validation do
        params { required(:title).filled }
      end

      property :composer, populate_if_empty: :populate_composer! do # lambda works, too. in form context.
        property :name
        validation do
          params { required(:name).filled }
        end
      end

    private
      def populate_composer!(options)
        Artist.new
      end
    end

    property :artist, populate_if_empty: ->(args) { create_artist(args[:fragment], args[:user_options]) } do # methods work, too.
      property :name
    end

    private
    class Sting < Artist
      attr_accessor :args
    end
    def create_artist(input, user_options)
      Sting.new.tap { |artist| artist.args = ([input, user_options].to_s) }
    end
  end

  let(:form) { AlbumForm.new(album) }

  it do
    assert_equal form.songs.size, 2

    assert form.validate(
      "songs" => [{"title" => "Fallout"}, {"title" => "Roxanne"},
        {"title" => "Rime Of The Ancient Mariner"}, # new song.
        {"title" => "Re-Education", "composer" => {"name" => "Rise Against"}}], # new song with new composer.
    )

    assert_equal form.errors.messages.inspect, "{}"

    # form has updated.
    assert_equal form.name, "The Dissent Of Man"
    assert_equal form.songs[0].title, "Fallout"
    assert_equal form.songs[1].title, "Roxanne"
    assert_equal form.songs[1].composer.name, "Greg Graffin"
    assert_equal form.songs[1].title, "Roxanne"
    assert_equal form.songs[2].title, "Rime Of The Ancient Mariner" # new song added.
    assert_equal form.songs[3].title, "Re-Education"
    assert_equal form.songs[3].composer.name, "Rise Against"
    assert_equal form.songs.size, 4
    assert_equal form.artist.name, "Bad Religion"

    # model has not changed, yet.
    assert_equal album.name, "The Dissent Of Man"
    assert_equal album.songs[0].title, "Broken"
    assert_equal album.songs[1].title, "Resist Stance"
    assert_equal album.songs[1].composer.name, "Greg Graffin"
    assert_equal album.songs.size, 2
    assert_equal album.artist.name, "Bad Religion"
  end

  # trigger artist populator. lambda calling form instance method.
  it "xxxx" do
    form = AlbumForm.new(album = Album.new)
    form.validate("artist" => {"name" => "From Autumn To Ashes"})

    assert_equal form.artist.name, "From Autumn To Ashes"
    # test lambda was executed in form context.
    assert form.artist.model.is_a? AlbumForm::Sting
    # test lambda block arguments.
    assert_equal form.artist.model.args.to_s, "[{\"name\"=>\"From Autumn To Ashes\"}, nil]"

    assert_nil album.artist
  end
end

# delete songs while deserializing.
class PopulateIfEmptyWithDeletionTest < MiniTest::Spec
  Song  = Struct.new(:title, :album, :composer)
  Album = Struct.new(:name, :songs, :artist)

  let(:song)  { Song.new("Broken") }
  let(:song2) { Song.new("Resist Stance") }
  let(:album) { Album.new("The Dissent Of Man", [song, song2]) }

  class AlbumForm < TestForm
    property :name

    collection :songs,
      populate_if_empty: Song, skip_if: :delete_song! do

      property :title
      validation do
        params { required(:title).filled }
      end
    end

    def delete_song!(options)
      songs.delete(songs[0]) and return true if options[:fragment]["title"] == "Broken, delete me!"
      false
    end
  end

  let(:form) { AlbumForm.new(album) }

  it do
    assert form.validate(
      "songs" => [{"title" => "Broken, delete me!"}, {"title" => "Roxanne"}]
    )

    assert_equal form.errors.messages.inspect, "{}"

    assert_equal form.songs.size, 1
    assert_equal form.songs[0].title, "Roxanne"
  end
end

class PopulateWithFormKeyTest < MiniTest::Spec
  Song  = Struct.new(:title, :album, :composer)
  Album = Struct.new(:name, :songs, :artist)

  let(:song)  { Song.new('Broken') }
  let(:song2) { Song.new('Resist Stance') }
  let(:album) { Album.new('The Dissent Of Man', [song, song2]) }

  class SongForm < TestForm
    property :title

    validation do
      params { required(:title).filled }
    end
  end

  class AlbumForm < TestForm
    property :name

    collection :songs, form: SongForm, populator: :populator!, model_identifier: :title

    def populator!(fragment:, **)
      item = songs.find { |song| song.title == fragment['title'] }
      if item && fragment['delete'] == '1'
        songs.delete(item)
        return skip!
      end
      item || songs.append(Song.new)
    end
  end

  let(:form) { AlbumForm.new(album) }

  it do
    assert_equal 2, form.songs.size

    assert form.validate(
      'songs' => [
        { 'title' => 'Broken' },
        { 'title' => 'Resist Stance' },
        { 'title' => 'Rime Of The Ancient Mariner' }
      ]
    )

    assert_equal 3, form.songs.size

    assert form.validate(
      'songs' => [
        { 'title' => 'Broken', 'delete' => '1' },
        { 'title' => 'Resist Stance' },
        { 'title' => 'Rime Of The Ancient Mariner' }
      ]
    )
    assert_equal 2, form.songs.size
    assert_equal 'Resist Stance', form.songs.first.title
    assert_equal 'Rime Of The Ancient Mariner', form.songs.last.title
  end
end
