require_relative "test_helper"

class CardsTest < Minitest::Test
  include BacklogTestHelpers

  def setup
    @client = FakePlankaClient.new
    @ensurer = Backlog::BoardEnsurer.new(client: @client, human_user_id: "human-1")
    @ensurer.ensure("llf-schema", project_name: "Dev")
    @cards = Backlog::Cards.new(client: @client)
  end

  def test_add_creates_card_at_backlog_list
    card = @cards.add(board_name: "llf-schema", title: "Fix the flaky import")

    backlog = @client.lists_store.find { |l| l.name == "Backlog" }
    assert_equal backlog.id, card.list_id
    assert_equal "Fix the flaky import", card.name
    assert_equal "project", card.type
  end

  def test_add_positions_new_card_after_existing_backlog_cards
    first = @cards.add(board_name: "llf-schema", title: "one")
    second = @cards.add(board_name: "llf-schema", title: "two")

    assert_operator second.position, :>, first.position
  end

  def test_add_with_description
    card = @cards.add(board_name: "llf-schema", title: "t", description: "details here")

    assert_equal "details here", card.description
  end

  def test_add_raises_for_missing_board
    error = assert_raises(RuntimeError) { @cards.add(board_name: "nope", title: "t") }
    assert_match(/no board named/, error.message)
  end

  def test_snapshot_shape_and_list_order
    @cards.add(board_name: "llf-schema", title: "captured task")

    snapshot = @cards.snapshot(board_name: "llf-schema")

    assert_equal "llf-schema", snapshot["board"]
    assert_equal Backlog::BoardSpec::LISTS, snapshot["lists"].map { |l| l["name"] }
    backlog = snapshot["lists"].find { |l| l["name"] == "Backlog" }
    assert_equal ["captured task"], backlog["cards"].map { |c| c["name"] }
    assert_equal Backlog::BoardSpec::LABEL_NAMES.sort, snapshot["labels"].sort
  end

  def test_snapshot_card_emits_complete_key_set
    @cards.add(board_name: "llf-schema", title: "captured task")

    snapshot = @cards.snapshot(board_name: "llf-schema")
    backlog = snapshot["lists"].find { |l| l["name"] == "Backlog" }
    card = backlog["cards"].first

    expected_keys = %w[
      id createdAt updatedAt type position name description dueDate
      isDueCompleted stopwatch commentsTotal isClosed listChangedAt
      boardId listId creatorUserId prevListId coverAttachmentId
      isSubscribed labels
    ]
    assert_equal expected_keys.sort, card.keys.sort
    assert card.key?("listChangedAt"), "snapshot card must carry listChangedAt for board-audit"
  end

  def test_snapshot_includes_card_label_names
    card = @cards.add(board_name: "llf-schema", title: "labeled task")
    p1 = @client.labels_store.find { |l| l.name == "P1" }
    @client.attach_label(card.id, p1.id)

    snapshot = @cards.snapshot(board_name: "llf-schema")

    backlog = snapshot["lists"].find { |l| l["name"] == "Backlog" }
    assert_equal ["P1"], backlog["cards"].first["labels"]
  end

  def test_snapshot_raises_for_missing_board
    assert_raises(RuntimeError) { @cards.snapshot(board_name: "nope") }
  end

  def test_attach_label_by_name
    card = @cards.add(board_name: "llf-schema", title: "raw card")

    @cards.attach_label(board_name: "llf-schema", card_id: card.id, label: "afk-ready")

    snapshot = @cards.snapshot(board_name: "llf-schema")
    backlog = snapshot["lists"].find { |l| l["name"] == "Backlog" }
    assert_equal ["afk-ready"], backlog["cards"].first["labels"]
  end

  def test_attach_label_raises_for_unknown_label
    card = @cards.add(board_name: "llf-schema", title: "raw card")

    error = assert_raises(RuntimeError) do
      @cards.attach_label(board_name: "llf-schema", card_id: card.id, label: "nonexistent")
    end
    assert_match(/no label/, error.message)
  end

  def test_comment_adds_text_to_card
    card = @cards.add(board_name: "llf-schema", title: "stale card")

    comment = @cards.comment(card_id: card.id, text: "audit: in Doing 9 days")

    assert_equal card.id, comment.card_id
    assert_equal "audit: in Doing 9 days", comment.text
    assert_equal 1, @client.comments.store.size
  end

  # -- move -----------------------------------------------------------

  def list_named(name)
    @client.lists_store.find { |l| l.name == name }
  end

  def move_card_to(list_name, card = nil)
    card ||= @cards.add(board_name: "llf-schema", title: "moving card")
    @client.cards.move(card.id, list_id: list_named(list_name).id, position: 65_536) unless list_name == "Backlog"
    card
  end

  def label!(card, name)
    label = @client.labels_store.find { |l| l.name == name }
    @client.attach_label(card.id, label.id)
  end

  def test_move_backlog_to_doing
    card = @cards.add(board_name: "llf-schema", title: "task")

    moved = @cards.move(board_name: "llf-schema", card_id: card.id, to: "Doing")

    assert_equal list_named("Doing").id, moved.list_id
  end

  def test_move_doing_to_review
    card = move_card_to("Doing")

    moved = @cards.move(board_name: "llf-schema", card_id: card.id, to: "Review")

    assert_equal list_named("Review").id, moved.list_id
  end

  def test_move_afk_ready_doing_to_done
    card = move_card_to("Doing")
    label!(card, "afk-ready")

    moved = @cards.move(board_name: "llf-schema", card_id: card.id, to: "Done")

    assert_equal list_named("Done").id, moved.list_id
  end

  def test_move_positions_card_at_end_of_destination_list
    first = move_card_to("Doing")
    second = @cards.add(board_name: "llf-schema", title: "second")

    @cards.move(board_name: "llf-schema", card_id: first.id, to: "Waiting")
    moved_second = @cards.move(board_name: "llf-schema", card_id: second.id, to: "Waiting")

    waiting_first = @client.cards.all_for_board(list_named("Waiting").board_id)
                            .find { |c| c.id == first.id }
    assert_operator moved_second.position, :>, waiting_first.position
  end

  def test_move_into_next_refused
    card = @cards.add(board_name: "llf-schema", title: "task")

    error = assert_raises(RuntimeError) do
      @cards.move(board_name: "llf-schema", card_id: card.id, to: "Next")
    end
    assert_match(/Next is human-curated/, error.message)
  end

  def test_move_out_of_next_refused
    card = move_card_to("Next")

    error = assert_raises(RuntimeError) do
      @cards.move(board_name: "llf-schema", card_id: card.id, to: "Doing")
    end
    assert_match(/Next is human-curated/, error.message)
  end

  def test_move_hitl_card_refused
    card = @cards.add(board_name: "llf-schema", title: "task")
    label!(card, "hitl")

    error = assert_raises(RuntimeError) do
      @cards.move(board_name: "llf-schema", card_id: card.id, to: "Doing")
    end
    assert_match(/hitl cards are human-owned/, error.message)
  end

  def test_move_to_done_without_afk_ready_refused
    card = move_card_to("Doing")

    error = assert_raises(RuntimeError) do
      @cards.move(board_name: "llf-schema", card_id: card.id, to: "Done")
    end
    assert_match(/afk-ready/, error.message)
  end

  def test_move_out_of_done_refused
    card = move_card_to("Doing")
    label!(card, "afk-ready")
    @cards.move(board_name: "llf-schema", card_id: card.id, to: "Done")

    error = assert_raises(RuntimeError) do
      @cards.move(board_name: "llf-schema", card_id: card.id, to: "Doing")
    end
    assert_match(/cannot move a card out of Done/, error.message)
  end

  def test_move_unknown_column_refused
    card = @cards.add(board_name: "llf-schema", title: "task")

    error = assert_raises(RuntimeError) do
      @cards.move(board_name: "llf-schema", card_id: card.id, to: "Someplace")
    end
    assert_match(/unknown list/, error.message)
  end
end
