# frozen_string_literal: true

def date_parsing_specs(test_stdout: true)
  describe "date parsing" do
    let(:description) { "Test." }

    describe "when date is in YYYY-MM-DD" do
      let(:date) { "2017-01-01" }

      it { line_added "- #{date}: #{description}" }
      it { stdout_only "#{capitalized_event} added: \"#{date}: #{description}\"" } if test_stdout
    end

    describe "when date is in MM-DD-YYYY" do
      let(:date) { "01-02-2017" }

      it { line_added "- 2017-01-02: #{description}" }
      it { stdout_only "#{capitalized_event} added: \"2017-01-02: #{description}\"" } if test_stdout
    end

    describe "when date is invalid" do
      let(:date) { "2017-02-30" }

      it { line_added "- 2017-03-02: #{description}" }
      it { stdout_only "#{capitalized_event} added: \"2017-03-02: #{description}\"" } if test_stdout
    end

    describe "when date is natural language and in full" do
      let(:date) { "February 23rd, 2017" }

      it { line_added "- 2017-02-23: #{description}" }
      it { stdout_only "#{capitalized_event} added: \"2017-02-23: #{description}\"" } if test_stdout
    end

    describe "when date is natural language and only month and day" do
      # We use two days rather than just one to avoid strange behavior around
      # edge cases when the test is being run right around midnight.
      let(:two_days_in_seconds) { 2 * 24 * 60 * 60 }
      let(:raw_date) { Time.now + two_days_in_seconds }
      let(:date) { raw_date.strftime("%B %d") }
      let(:expected_year) { raw_date.strftime("%Y").to_i - 1 }
      let(:expected_date_str) { "#{expected_year}-#{raw_date.strftime('%m-%d')}" }

      it { line_added "- #{expected_date_str}: #{description}" }
      if test_stdout
        it { stdout_only "#{capitalized_event} added: \"#{expected_date_str}: #{description}\"" }
      end
    end
  end
end

def description_parsing_specs(test_stdout: true)
  describe "description parsing" do
    let(:date) { Date.today.strftime }

    unless test_stdout
      describe "when description is blank" do
        let(:description) { "  " }

        it "prints an error message" do
          value(subject[:stderr]).must_equal(
            ensure_trailing_newline_unless_empty("Error: Blank #{event} not added")
          )
        end
      end
    end

    describe "when description includes a friend's full name (case insensitive)" do
      let(:description) { "Lunch with grace hopper." }

      it { line_added "- #{date}: Lunch with **Grace Hopper**." }
      if test_stdout
        it { stdout_only "#{capitalized_event} added: \"#{date}: Lunch with Grace Hopper.\"" }
      end
    end

    describe "when description includes a friend's first name (case insensitive)" do
      let(:description) { "Lunch with grace." }

      it { line_added "- #{date}: Lunch with **Grace Hopper**." }
      if test_stdout
        it { stdout_only "#{capitalized_event} added: \"#{date}: Lunch with Grace Hopper.\"" }
      end
    end

    describe "when description has a friend's first name and last initial (case insensitive)" do
      describe "when followed by no period" do
        let(:description) { "Lunch with grace h" }

        it { line_added "- #{date}: Lunch with **Grace Hopper**" }
        if test_stdout
          it { stdout_only "#{capitalized_event} added: \"#{date}: Lunch with Grace Hopper\"" }
        end
      end

      describe "when followed by a period at the end of a sentence" do
        let(:description) { "Met grace h. So fun!" }

        it { line_added "- #{date}: Met **Grace Hopper**. So fun!" }
        if test_stdout
          it { stdout_only "#{capitalized_event} added: \"#{date}: Met Grace Hopper. So fun!\"" }
        end
      end

      describe "when followed by a period at the end of the description" do
        let(:description) { "Lunch with grace h." }

        it { line_added "- #{date}: Lunch with **Grace Hopper**." }
        if test_stdout
          it { stdout_only "#{capitalized_event} added: \"#{date}: Lunch with Grace Hopper.\"" }
        end
      end

      describe "when followed by a period in the middle of a sentence" do
        let(:description) { "Met grace h. at 12." }

        it { line_added "- #{date}: Met **Grace Hopper** at 12." }
        if test_stdout
          it { stdout_only "#{capitalized_event} added: \"#{date}: Met Grace Hopper at 12.\"" }
        end
      end

      describe "when followed by a period and a comma" do
        let(:description) { "Met grace h., and others, at 12." }

        it { line_added "- #{date}: Met **Grace Hopper**, and others, at 12." }
        if test_stdout
          it { stdout_only "#{capitalized_event} added: \"#{date}: Met Grace Hopper, and others, at 12.\"" } # rubocop:disable Layout/LineLength
        end
      end

      describe "when followed by a period, a comma, and a proper noun" do
        let(:description) { "Met grace h., King James, and others at 12." }

        it { line_added "- #{date}: Met **Grace Hopper**, King James, and others at 12." }
        if test_stdout
          it { stdout_only "#{capitalized_event} added: \"#{date}: Met Grace Hopper, King James, and others at 12.\"" } # rubocop:disable Layout/LineLength
        end
      end

      describe "when followed by a period and a complex series of sentence-ending punctuation" do
        let(:description) { "Met someone—grace h.?! At 12." }

        it { line_added "- #{date}: Met someone—**Grace Hopper**?! At 12." }
        if test_stdout
          it { stdout_only "#{capitalized_event} added: \"#{date}: Met someone—Grace Hopper?! At 12.\"" } # rubocop:disable Layout/LineLength
        end
      end

      describe "when followed by a period and a complex series of mid-sentence punctuation" do
        let(:description) { "Met someone {grace h.}—at 12." }

        it { line_added "- #{date}: Met someone {**Grace Hopper**}—at 12." }
        if test_stdout
          it { stdout_only "#{capitalized_event} added: \"#{date}: Met someone {Grace Hopper}—at 12.\"" } # rubocop:disable Layout/LineLength
        end
      end

      describe "when followed by a period as part of a sentence-ending ellipsis" do
        let(:description) { "Met grace h... Great!" }

        it { line_added "- #{date}: Met **Grace Hopper**... Great!" }
        if test_stdout
          it { stdout_only "#{capitalized_event} added: \"#{date}: Met Grace Hopper... Great!\"" }
        end
      end

      describe "when followed by a period as part of a mid-sentence ellipsis" do
        let(:description) { "Met grace h... at 12." }

        it { line_added "- #{date}: Met **Grace Hopper**... at 12." }
        if test_stdout
          it { stdout_only "#{capitalized_event} added: \"#{date}: Met Grace Hopper... at 12.\"" }
        end
      end
    end

    describe "when description includes a friend's nickname (case insensitive)" do
      let(:description) { "Lunch with the admiral." }

      it { line_added "- #{date}: Lunch with **Grace Hopper**." }
      if test_stdout
        it { stdout_only "#{capitalized_event} added: \"#{date}: Lunch with Grace Hopper.\"" }
      end
    end

    describe "when description includes a friend's nickname which contains a name" do
      let(:description) { "Lunch with Amazing Grace." }

      it { line_added "- #{date}: Lunch with **Grace Hopper**." }
      if test_stdout
        it { stdout_only "#{capitalized_event} added: \"#{date}: Lunch with Grace Hopper.\"" }
      end
    end

    describe "when description includes a friend's name at the beginning of a word" do
      # Capitalization reduces chance of a false positive.
      let(:description) { "Gracefully strolled." }

      it { line_added "- #{date}: Gracefully strolled." }
      if test_stdout
        it { stdout_only "#{capitalized_event} added: \"#{date}: Gracefully strolled.\"" }
      end
    end

    describe "when description includes a friend's name at the end of a word" do
      # Capitalization reduces chance of a false positive.
      let(:description) { "The service was a disGrace." }

      it { line_added "- #{date}: The service was a disGrace." }
      if test_stdout
        it { stdout_only "#{capitalized_event} added: \"#{date}: The service was a disGrace.\"" }
      end
    end

    describe "when description includes a friend's name in the middle of a word" do
      # Capitalization reduces chance of a false positive.
      let(:description) { "The service was disGraceful." }

      it { line_added "- #{date}: The service was disGraceful." }
      if test_stdout
        it { stdout_only "#{capitalized_event} added: \"#{date}: The service was disGraceful.\"" }
      end
    end

    describe "when a friend's name is escaped with a backslash" do
      # We have to use four backslashes here because of Ruby's backslash escaping; when this
      # goes through all of the layers of this test it emerges on the other side as a single one.
      let(:description) { "Dinner with \\\\Grace Kelly." }

      it { line_added "- #{date}: Dinner with Grace Kelly." }
      if test_stdout
        it { stdout_only "#{capitalized_event} added: \"#{date}: Dinner with Grace Kelly.\"" }
      end
    end

    describe "hyphenated name edge cases" do
      describe "when one name precedes another before a hyphen" do
        let(:description) { "Shopped w/ Mary-Kate." }

        # Make sure "Mary" is a closer friend than "Mary-Kate" so we know our
        # test result isn't due to chance.
        let(:content) do
          <<-FILE
### Activities:
- 2017-01-01: Singing with **Mary Poppins**.

### Notes:

### Friends:
- Mary Poppins
- Mary-Kate Olsen

### Locations:
FILE
        end

        it { line_added "- #{date}: Shopped w/ **Mary-Kate Olsen**." }
        if test_stdout
          it { stdout_only "#{capitalized_event} added: \"#{date}: Shopped w/ Mary-Kate Olsen.\"" }
        end
      end

      describe "when one name follows another after a hyphen" do
        let(:description) { "Shopped w/ Mary-Kate." }

        # Make sure "Kate" is a closer friend than "Mary-Kate" so we know our
        # test result isn't due to chance.
        let(:content) do
          <<-FILE
### Activities:
- 2017-01-01: Improv with **Kate Winslet**.

### Notes:

### Friends:
- Kate Winslet
- Mary-Kate Olsen

### Locations:
FILE
        end

        it { line_added "- #{date}: Shopped w/ **Mary-Kate Olsen**." }
        if test_stdout
          it { stdout_only "#{capitalized_event} added: \"#{date}: Shopped w/ Mary-Kate Olsen.\"" }
        end
      end

      describe "when one name is contained within another via hyphens" do
        let(:description) { "Met Mary-Jo-Kate." }

        # Make sure "Jo" is a closer friend than "Mary-Jo-Kate" so we know our
        # test result isn't due to chance.
        let(:content) do
          <<-FILE
### Activities:
- 2017-01-01: Singing with **Jo Stafford**.

### Notes:

### Friends:
- Jo Stafford
- Mary-Jo-Kate Olsen

### Locations:
FILE
        end

        it { line_added "- #{date}: Met **Mary-Jo-Kate Olsen**." }
        if test_stdout
          it { stdout_only "#{capitalized_event} added: \"#{date}: Met Mary-Jo-Kate Olsen.\"" }
        end
      end
    end

    describe "when a friend's first and middle name matches two other friends' first names" do
      let(:description) { "Met Martin Luther." }

      # Make sure "Martin Jones" and "Luther Jones" are closer friends than
      # "Martin Luther King" so we know our test result isn't due to chance.
      let(:content) do
        <<-FILE
### Activities:
- 2018-01-01: Singing with **Martin Jones**.
- 2018-01-01: Dancing with **Luther Jones**.

### Notes:

### Friends:
- Luther Jones
- Martin Jones
- Martin Luther King

### Locations:
FILE
      end

      it { line_added "- #{date}: Met **Martin Luther King**." }
      if test_stdout
        it { stdout_only "#{capitalized_event} added: \"#{date}: Met Martin Luther King.\"" }
      end
    end

    describe "when a friend has a first name only" do
      # We want to explicitly check that the first-name last-initial behavior
      # doesn't kick in here and match "Alejandra A" when the friend has no
      # last name.
      let(:description) { "Met Alejandra a few times." }

      let(:content) do
        <<-FILE
### Activities:

### Notes:

### Friends:
- Alejandra

### Locations:
FILE
      end

      it { line_added "- #{date}: Met **Alejandra** a few times." }
      if test_stdout
        it { stdout_only "#{capitalized_event} added: \"#{date}: Met Alejandra a few times.\"" }
      end
    end

    describe "when description has a friend's name with leading asterisks" do
      let(:description) { "Lunch with **Grace Hopper." }

      it { line_added "- #{date}: Lunch with **Grace Hopper." }
      if test_stdout
        it { stdout_only "#{capitalized_event} added: \"#{date}: Lunch with **Grace Hopper.\"" }
      end
    end

    describe "when description has a friend's name with trailing asterisks" do
      # Note: We can't guarantee that "Grace Hopper**" doesn't match because the "Grace" isn't
      # surrounded by asterisks.
      let(:description) { "Lunch with Grace**." }

      it { line_added "- #{date}: Lunch with Grace**." }
      if test_stdout
        it { stdout_only "#{capitalized_event} added: \"#{date}: Lunch with Grace**.\"" }
      end
    end

    describe "when description has a friend's name multiple times" do
      let(:description) { "Grace! Grace!!!" }

      it { line_added "- #{date}: **Grace Hopper**! **Grace Hopper**!!!" }
      if test_stdout
        it { stdout_only "#{capitalized_event} added: \"#{date}: Grace Hopper! Grace Hopper!!!\"" }
      end
    end

    describe "when description has a friend's full name which is another friend's first name" do
      let(:description) { "Hung out with Elizabeth for most of the day." }

      # Make sure "Elizabeth II" is a better friend than just "Elizabeth" to
      # ensure our result isn't due to chance.
      let(:content) do
        <<-FILE
### Activities:
- 2018-08-05: Royal picnic with **Elizabeth II**.

### Notes:

### Friends:
- Elizabeth
- Elizabeth II

### Locations:
FILE
      end

      it { line_added "- #{date}: Hung out with **Elizabeth** for most of the day." }
      if test_stdout
        it do
          stdout_only "#{capitalized_event} added: \"#{date}: "\
                      "Hung out with Elizabeth for most of the day.\""
        end
      end
    end

    describe "when description has a name with multiple friend matches" do
      describe "when there is useful context from past activities" do
        let(:description) { "Met John + Elizabeth." }

        # Create a past activity in which Elizabeth Cady Stanton did something
        # with John Cage. Then, create past activities to make Elizabeth II a
        # better friend than Elizabeth Cady Stanton.
        let(:content) do
          <<-FILE
### Activities:
- 2017-01-05: Picnic with **Elizabeth Cady Stanton** and **John Cage**.
- 2017-01-04: Got lunch with **Elizabeth II**.
- 2017-01-03: Ice skated with **Elizabeth II**.

### Notes:

### Friends:
- Elizabeth Cady Stanton
- Elizabeth II
- John Cage

### Locations:
FILE
        end

        # Elizabeth II is the better friend, but historical activities have
        # had Elizabeth Cady Stanton and John Cage together. Thus, we should
        # interpret "Elizabeth" as Elizabeth Cady Stanton.
        it { line_added "- #{date}: Met **John Cage** + **Elizabeth Cady Stanton**." }
        if test_stdout
          it do
            stdout_only "#{capitalized_event} added: "\
                        "\"#{date}: Met John Cage + Elizabeth Cady Stanton.\""
          end
        end
      end

      describe "when there is no useful context from past activities" do
        let(:description) { "Dinner with John and Elizabeth." }

        # Create a past activity in which Elizabeth Cady Stanton did something
        # with John Cage. Then, create past activities to make Elizabeth II a
        # better friend than Elizabeth Cady Stanton.
        let(:content) do
          <<-FILE
### Activities:
- 2017-01-03: Ice skated with **Elizabeth II**.

### Notes:

### Friends:
- Elizabeth Cady Stanton
- Elizabeth II
- John Cage

### Locations:
FILE
        end

        # Pick the "Elizabeth" with more activities.
        it { line_added "- #{date}: Dinner with **John Cage** and **Elizabeth II**." }
        if test_stdout
          it do
            stdout_only "#{capitalized_event} added: "\
                        "\"#{date}: Dinner with John Cage and Elizabeth II.\""
          end
        end
      end
    end

    describe "when description contains a location name (case insensitive)" do
      let(:description) { "Lunch at a cafe in paris." }

      it { line_added "- #{date}: Lunch at a cafe in _Paris_." }
      if test_stdout
        it { stdout_only "#{capitalized_event} added: \"#{date}: Lunch at a cafe in Paris.\"" }
      end
    end

    describe "when description contains both names and locations" do
      let(:description) { "Grace and I visited Atlantis and then Paris for lunch with George." }

      it do
        line_added "- #{date}: **Grace Hopper** and I visited _Atlantis_ and then _Paris_ for "\
                   "lunch with **George Washington Carver**."
      end
      if test_stdout
        it do
          stdout_only "#{capitalized_event} added: \"#{date}: Grace Hopper and I visited "\
                      "Atlantis and then Paris for lunch with George Washington Carver.\""
        end
      end
    end
  end
end

def parsing_specs(event:)
  let(:event) { event.to_s }
  let(:capitalized_event) { event.to_s.capitalize }

  describe "when given a date and a description in the command" do
    subject { run_cmd("add #{event} #{date}: #{description}") }

    date_parsing_specs
    description_parsing_specs
  end

  describe "when given only a date in the command" do
    subject { run_cmd("add #{event} #{date}", stdin_data: description) }

    # We don't try to test the STDOUT here because our command prompt produces other STDOUT that's
    # hard to test.
    date_parsing_specs(test_stdout: false)
    description_parsing_specs(test_stdout: false)
  end

  describe "when given only a description in the command" do
    subject { run_cmd("add #{event} #{description}") }

    # We don't test date parsing since in this case the date is always inferred to be today.

    description_parsing_specs
  end

  describe "when given neither a date nor a description in the command" do
    subject { run_cmd("add #{event}", stdin_data: description) }

    # We don't test date parsing since in this case the date is always inferred to be today.

    # We don't try to test the STDOUT here because our command prompt produces other STDOUT that's
    # hard to test.
    description_parsing_specs(test_stdout: false)
  end
end
