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

module OrderExport
  Line = Data.define(:name, :quantity, :unit_cents)
  module_function

  def normalize(raw)
    raise ArgumentError, "row must be a Hash" unless raw.is_a?(Hash)
    name = raw.fetch(:name)
    quantity = raw.fetch(:quantity)
    cents = raw.fetch(:unit_cents)
    raise ArgumentError, "name must be nonblank" unless name.is_a?(String) && !name.strip.empty?
    raise ArgumentError, "quantity must be a nonnegative Integer" unless quantity.is_a?(Integer) && quantity >= 0
    raise ArgumentError, "unit_cents must be a nonnegative Integer" unless cents.is_a?(Integer) && cents >= 0

    Line.new(name.strip.freeze, quantity, cents)
  end

  def lines(rows)
    rows.map { |raw| normalize(raw) }.select { |line| line.quantity.positive? }.freeze
  end

  def total_cents(lines)
    lines.sum { |line| line.quantity * line.unit_cents }
  end

  def to_csv(lines)
    CSV.generate do |csv|
      csv << %w[name quantity total_cents]
      lines.each { |line| csv << [line.name, line.quantity, line.quantity * line.unit_cents] }
    end
  end

  def first_positive(rows, count:)
    raise ArgumentError, "count must be nonnegative" unless count.is_a?(Integer) && count >= 0
    rows.lazy.map { |raw| normalize(raw) }
        .select { |line| line.quantity.positive? }.take(count).force
  end
end

class OrderExportTest < Minitest::Test
  def row(name: " tea ", quantity: 2, unit_cents: 125)
    { name: name, quantity: quantity, unit_cents: unit_cents }
  end

  def test_pipeline_total_and_empty_input
    lines = OrderExport.lines([row, row(quantity: 0), row(name: "cake", quantity: 1, unit_cents: 250)])
    assert_equal ["tea", "cake"], lines.map(&:name)
    assert_equal 500, OrderExport.total_cents(lines)
    assert_equal 0, OrderExport.total_cents([])
  end

  def test_normalization_does_not_change_or_share_the_input_name
    input = row(name: +"tea")
    output = OrderExport.normalize(input)
    input[:name] << "!"
    assert_equal "tea", output.name
    assert_equal "tea!", input[:name]
    assert output.frozen?
    assert output.name.frozen?
  end

  def test_frozen_input_is_accepted
    input = row(name: " tea ".freeze).freeze
    assert_equal "tea", OrderExport.normalize(input).name
    assert_equal " tea ", input[:name]
  end

  def test_csv_round_trip_handles_commas_quotes_and_newlines
    name = "Tea, \"Earl\"\nGrey"
    csv = OrderExport.to_csv(OrderExport.lines([row(name: name)]))
    assert_equal [["name", "quantity", "total_cents"], [name, "2", "250"]], CSV.parse(csv)
  end

  def test_invalid_inputs_are_not_silently_coerced
    [nil, "", 42].each { |name| assert_raises(ArgumentError) { OrderExport.normalize(row(name: name)) } }
    ["2", -1, 1.5, nil].each { |q| assert_raises(ArgumentError) { OrderExport.normalize(row(quantity: q)) } }
    assert_raises(KeyError) { OrderExport.normalize({}) }
    assert_raises(ArgumentError) { OrderExport.normalize([]) }
    assert_raises(ArgumentError) { OrderExport.normalize(row(unit_cents: -1)) }
  end

  def test_lazy_pipeline_stops_consuming_after_requested_match
    visited = []
    source = Enumerator.new do |y|
      [0, 2, 3, 4].each { |q| visited << q; y << row(quantity: q) }
    end
    assert_equal [2], OrderExport.first_positive(source, count: 1).map(&:quantity)
    assert_equal [0, 2], visited
    assert_raises(ArgumentError) { OrderExport.first_positive(source, count: -1) }
  end

  def test_freeze_is_shallow_counterexample
    inner = []
    outer = [inner].freeze
    inner << :changed
    assert_equal [[:changed]], outer
    assert_raises(FrozenError) { outer << [] }
  end

  def test_repeatability_and_output_freeze
    input = [row]
    assert_equal OrderExport.to_csv(OrderExport.lines(input)), OrderExport.to_csv(OrderExport.lines(input))
    assert_raises(FrozenError) { OrderExport.lines(input) << OrderExport.normalize(row) }
  end
end
