# frozen_string_literal: true
gem "minitest", "6.0.6"
require "minitest/autorun"

module EditorLab
  class Document
    attr_reader :text
    def initialize(text = "")
      @text = String(text).dup.freeze
    end
    def replace(text)
      @text = String(text).dup.freeze
    end
  end

  class Append
    def initialize(document, suffix)
      @document, @suffix = document, String(suffix).dup.freeze
      @state = :new
    end

    def execute
      raise "command already active" unless [:new, :undone].include?(@state)
      before = @document.text
      raise "document diverged" if @state == :undone && before != @before
      @before = before
      @after = before + @suffix
      @document.replace(@after)
      @state = :done
    end

    def undo
      raise "command is not active" unless @state == :done
      raise "document diverged" unless @document.text == @after
      @document.replace(@before)
      @state = :undone
    end
  end

  class History
    attr_reader :cursor
    def initialize
      @commands = []
      @cursor = 0
    end

    def execute(command)
      raise ArgumentError, "use a fresh command" if @commands.any? { |old| old.equal?(command) }
      command.execute
      @commands = @commands.take(@cursor) + [command]
      @cursor += 1
      true
    end

    def undo
      return false if @cursor.zero?
      @commands.fetch(@cursor - 1).undo
      @cursor -= 1
      true
    end

    def redo
      return false if @cursor == @commands.length
      @commands.fetch(@cursor).execute
      @cursor += 1
      true
    end

    def size = @commands.length
  end
end

class HistoryTest < Minitest::Test
  include EditorLab
  def setup
    @document = Document.new
    @history = History.new
  end
  def append(text)
    @history.execute(Append.new(@document, text))
  end
  def test_undo_and_redo_restore_exact_content
    append("a"); append("b")
    assert_equal "ab", @document.text
    assert @history.undo
    assert_equal "a", @document.text
    assert @history.redo
    assert_equal "ab", @document.text
  end
  def test_undo_everything_then_branch_discards_all_redo
    append("a"); append("b")
    2.times { @history.undo }
    assert_equal "", @document.text
    assert_equal 0, @history.cursor
    append("x")
    assert_equal 1, @history.size
    refute @history.redo
    @history.undo
    assert_equal "", @document.text
    @history.redo
    assert_equal "x", @document.text
  end
  def test_branch_in_middle_retains_only_applied_prefix
    append("a"); append("b"); append("c")
    2.times { @history.undo }; append("x")
    assert_equal "ax", @document.text
    assert_equal 2, @history.size
    2.times { @history.undo }
    assert_equal "", @document.text
  end
  def test_empty_history_boundaries
    refute @history.undo
    refute @history.redo
    assert_equal 0, @history.cursor
  end
  def test_external_edit_is_detected_without_moving_cursor
    append("a")
    @document.replace("external")
    assert_raises(RuntimeError) { @history.undo }
    assert_equal "external", @document.text
    assert_equal 1, @history.cursor
  end
  def test_failed_command_preserves_redo_branch
    append("a"); @history.undo
    command = Object.new
    def command.execute = raise("before any effect")
    assert_raises(RuntimeError) { @history.execute(command) }
    assert_equal 0, @history.cursor
    assert @history.redo
    assert_equal "a", @document.text
  end
  def test_inputs_are_copied_and_command_reuse_rejected
    suffix = +"a"; command = Append.new(@document, suffix); suffix << "b"
    @history.execute(command)
    assert_equal "a", @document.text
    assert_raises(ArgumentError) { @history.execute(command) }
    assert_raises(FrozenError) { @document.text << "x" }
  end
  def test_negative_index_slice_is_not_an_empty_prefix
    assert_equal [:a, :b], [:a, :b][0..-1]
    assert_equal [], [:a, :b].take(0)
  end
end
