# frozen_string_literal: true

require 'spec_helper'
require 'cucumber/formatter/spec_helper'
require 'cucumber/formatter/json'
require 'cucumber/cli/options'
require 'json'

module Cucumber
  module Formatter
    describe Json do
      extend SpecHelperDsl
      include SpecHelper

      before(:each) do
        @out = StringIO.new
        @formatter = described_class.new(actual_runtime.configuration.with_options(out_stream: @out))
        run_defined_feature
      end

      describe 'with a scenario with an undefined step' do
        define_feature <<~FEATURE
          Feature: Banana party

            Scenario: Monkey eats bananas
              Given there are bananas
        FEATURE

        it 'outputs the json data' do
          expect(load_normalised_json(@out)).to eq JSON.parse(%(
              [{"id": "banana-party",
                "uri": "spec.feature",
                "keyword": "Feature",
                "name": "Banana party",
                "line": 1,
                "description": "",
                "elements":
                 [{"id": "banana-party;monkey-eats-bananas",
                   "keyword": "Scenario",
                   "name": "Monkey eats bananas",
                   "line": 3,
                   "description": "",
                   "type": "scenario",
                   "steps":
                    [{"keyword": "Given ",
                      "name": "there are bananas",
                      "line": 4,
                      "match": {"location": "spec.feature:4"},
                      "result": {"status": "undefined"}}]}]}]))
        end
      end

      describe 'with a scenario with a passed step' do
        define_feature <<~FEATURE
          Feature: Banana party

            Scenario: Monkey eats bananas
              Given there are bananas
        FEATURE

        define_steps do
          Given(/^there are bananas$/) {}
        end

        it 'outputs the json data' do
          expect(load_normalised_json(@out)).to eq JSON.parse(%(
              [{"id": "banana-party",
                "uri": "spec.feature",
                "keyword": "Feature",
                "name": "Banana party",
                "line": 1,
                "description": "",
                "elements":
                 [{"id": "banana-party;monkey-eats-bananas",
                   "keyword": "Scenario",
                   "name": "Monkey eats bananas",
                   "line": 3,
                   "description": "",
                   "type": "scenario",
                   "steps":
                    [{"keyword": "Given ",
                      "name": "there are bananas",
                      "line": 4,
                      "match": {"location": "spec/cucumber/formatter/json_spec.rb:62"},
                      "result": {"status": "passed",
                                 "duration": 1}}]}]}]))
        end
      end

      describe 'with a scenario with a failed step' do
        define_feature <<~FEATURE
          Feature: Banana party

            Scenario: Monkey eats bananas
              Given there are bananas
        FEATURE

        define_steps do
          Given(/^there are bananas$/) { raise 'no bananas' }
        end

        it 'outputs the json data' do
          expect(load_normalised_json(@out)).to eq JSON.parse(%{
              [{"id": "banana-party",
                "uri": "spec.feature",
                "keyword": "Feature",
                "name": "Banana party",
                "line": 1,
                "description": "",
                "elements":
                 [{"id": "banana-party;monkey-eats-bananas",
                   "keyword": "Scenario",
                   "name": "Monkey eats bananas",
                   "line": 3,
                   "description": "",
                   "type": "scenario",
                   "steps":
                    [{"keyword": "Given ",
                      "name": "there are bananas",
                      "line": 4,
                      "match": {"location": "spec/cucumber/formatter/json_spec.rb:99"},
                      "result": {"status": "failed",
                                 "error_message": "no bananas (RuntimeError)\\n./spec/cucumber/formatter/json_spec.rb:99:in `/^there are bananas$/'\\nspec.feature:4:in `there are bananas'",
                                 "duration": 1}}]}]}]})
        end
      end

      describe 'with a scenario with a pending step' do
        define_feature <<~FEATURE
          Feature: Banana party

            Scenario: Monkey eats bananas
              Given there are bananas
        FEATURE

        define_steps do
          Given(/^there are bananas$/) { pending }
        end

        it 'outputs the json data' do
          expect(load_normalised_json(@out)).to eq JSON.parse(%{
              [{"id": "banana-party",
                "uri": "spec.feature",
                "keyword": "Feature",
                "name": "Banana party",
                "line": 1,
                "description": "",
                "elements":
                 [{"id": "banana-party;monkey-eats-bananas",
                   "keyword": "Scenario",
                   "name": "Monkey eats bananas",
                   "line": 3,
                   "description": "",
                   "type": "scenario",
                   "steps":
                    [{"keyword": "Given ",
                      "name": "there are bananas",
                      "line": 4,
                      "match": {"location": "spec/cucumber/formatter/json_spec.rb:137"},
                      "result": {"status": "pending",
                                 "error_message": "TODO (Cucumber::Pending)\\n./spec/cucumber/formatter/json_spec.rb:137:in `/^there are bananas$/'\\nspec.feature:4:in `there are bananas'",
                                 "duration": 1}}]}]}]})
        end
      end

      describe 'with a scenario outline with one example' do
        define_feature <<~FEATURE
          Feature: Banana party

            Scenario Outline: Monkey eats <fruit>
              Given there are <fruit>

              Examples: Fruit Table
              |  fruit  |
              | bananas |
        FEATURE

        define_steps do
          Given(/^there are bananas$/) {}
        end

        it 'outputs the json data' do
          expect(load_normalised_json(@out)).to eq JSON.parse(%(
              [{"id": "banana-party",
                "uri": "spec.feature",
                "keyword": "Feature",
                "name": "Banana party",
                "line": 1,
                "description": "",
                "elements":
                 [{"id": "banana-party;monkey-eats-<fruit>;fruit-table;2",
                   "keyword": "Scenario Outline",
                   "name": "Monkey eats bananas",
                   "line": 8,
                   "description": "",
                   "type": "scenario",
                   "steps":
                    [{"keyword": "Given ",
                      "name": "there are bananas",
                      "line": 4,
                      "match": {"location": "spec/cucumber/formatter/json_spec.rb:179"},
                      "result": {"status": "passed",
                                 "duration": 1}}]}]}]))
        end
      end

      describe 'with tags in the feature file' do
        define_feature <<~FEATURE
          @f
          Feature: Banana party

            @s
            Scenario: Monkey eats bananas
              Given there are bananas

            @so
            Scenario Outline: Monkey eats bananas
              Given there are <fruit>

              @ex
              Examples: Fruit Table
              |  fruit  |
              | bananas |
        FEATURE

        define_steps do
          Given(/^there are bananas$/) {}
        end

        it 'the tags are included in the json data' do
          expect(load_normalised_json(@out)).to eq JSON.parse(%(
              [{"id": "banana-party",
                "uri": "spec.feature",
                "keyword": "Feature",
                "name": "Banana party",
                "line": 2,
                "description": "",
                "tags": [{"name": "@f",
                          "line": 1}],
                "elements":
                 [{"id": "banana-party;monkey-eats-bananas",
                   "keyword": "Scenario",
                   "name": "Monkey eats bananas",
                   "line": 5,
                   "description": "",
                   "tags": [{"name": "@f",
                             "line": 1},
                            {"name": "@s",
                             "line": 4}],
                   "type": "scenario",
                   "steps":
                    [{"keyword": "Given ",
                      "name": "there are bananas",
                      "line": 6,
                      "match": {"location": "spec/cucumber/formatter/json_spec.rb:227"},
                      "result": {"status": "passed",
                                 "duration": 1}}]},
                   {"id": "banana-party;monkey-eats-bananas;fruit-table;2",
                   "keyword": "Scenario Outline",
                   "name": "Monkey eats bananas",
                   "line": 15,
                   "description": "",
                   "tags": [{"name": "@f",
                             "line": 1},
                            {"name": "@so",
                             "line": 8},
                            {"name": "@ex",
                             "line": 12}],
                   "type": "scenario",
                   "steps":
                    [{"keyword": "Given ",
                      "name": "there are bananas",
                      "line": 10,
                      "match": {"location": "spec/cucumber/formatter/json_spec.rb:227"},
                      "result": {"status": "passed",
                                 "duration": 1}}]}]}]))
        end
      end

      describe 'with comments in the feature file' do
        define_feature <<~FEATURE
          #feature comment
          Feature: Banana party

            #background comment
            Background: There are bananas
              Given there are bananas

            #scenario comment
            Scenario: Monkey eats bananas
              #step comment1
              Then the monkey eats bananas

            #scenario outline comment
            Scenario Outline: Monkey eats bananas
              #step comment2
              Then the monkey eats <fruit>

              #examples table comment
              Examples: Fruit Table
              |  fruit  |
              #examples table row comment
              | bananas |
        FEATURE

        define_steps do
          Given(/^there are bananas$/) {}
          Then(/^the monkey eats bananas$/) {}
        end

        it 'the comments are not included in the json data' do
          expect(load_normalised_json(@out)).to eq JSON.parse(%(
              [{"id": "banana-party",
                "uri": "spec.feature",
                "keyword": "Feature",
                "name": "Banana party",
                "line": 2,
                "description": "",
                "elements":
                 [{"keyword": "Background",
                   "name": "There are bananas",
                   "line": 5,
                   "description": "",
                   "type": "background",
                   "steps":
                    [{"keyword": "Given ",
                      "name": "there are bananas",
                      "line": 6,
                      "match": {"location": "spec/cucumber/formatter/json_spec.rb:307"},
                      "result": {"status": "passed",
                                 "duration": 1}}]},
                  {"id": "banana-party;monkey-eats-bananas",
                   "keyword": "Scenario",
                   "name": "Monkey eats bananas",
                   "line": 9,
                   "description": "",
                   "type": "scenario",
                   "steps":
                    [{"keyword": "Then ",
                      "name": "the monkey eats bananas",
                      "line": 11,
                      "match": {"location": "spec/cucumber/formatter/json_spec.rb:308"},
                      "result": {"status": "passed",
                                 "duration": 1}}]},
                  {"keyword": "Background",
                   "name": "There are bananas",
                   "line": 5,
                   "description": "",
                   "type": "background",
                   "steps":
                    [{"keyword": "Given ",
                      "name": "there are bananas",
                      "line": 6,
                      "match": {"location": "spec/cucumber/formatter/json_spec.rb:307"},
                      "result": {"status": "passed",
                                 "duration": 1}}]},
                   {"id": "banana-party;monkey-eats-bananas;fruit-table;2",
                   "keyword": "Scenario Outline",
                   "name": "Monkey eats bananas",
                   "line": 22,
                   "description": "",
                   "type": "scenario",
                   "steps":
                    [{"keyword": "Then ",
                      "name": "the monkey eats bananas",
                      "line": 16,
                      "match": {"location": "spec/cucumber/formatter/json_spec.rb:308"},
                      "result": {"status": "passed",
                                 "duration": 1}}]}]}]))
        end
      end

      describe 'with a scenario with a step with a doc string' do
        define_feature <<~FEATURE
          Feature: Banana party

            Scenario: Monkey eats bananas
              Given there are bananas
                """
                the doc string
                """
        FEATURE

        define_steps do
          Given(/^there are bananas$/) { |s| s }
        end

        it 'includes the doc string in the json data' do
          expect(load_normalised_json(@out)).to eq JSON.parse(%(
              [{"id": "banana-party",
                "uri": "spec.feature",
                "keyword": "Feature",
                "name": "Banana party",
                "line": 1,
                "description": "",
                "elements":
                 [{"id": "banana-party;monkey-eats-bananas",
                   "keyword": "Scenario",
                   "name": "Monkey eats bananas",
                   "line": 3,
                   "description": "",
                   "type": "scenario",
                   "steps":
                    [{"keyword": "Given ",
                      "name": "there are bananas",
                      "line": 4,
                      "doc_string": {"value": "the doc string",
                                     "content_type": "",
                                     "line": 5},
                      "match": {"location": "spec/cucumber/formatter/json_spec.rb:385"},
                      "result": {"status": "passed",
                                 "duration": 1}}]}]}]))
        end
      end

      describe 'with a scenario with a step that use puts' do
        define_feature <<~FEATURE
          Feature: Banana party

            Scenario: Monkey eats bananas
              Given there are bananas
        FEATURE

        define_steps do
          Given(/^there are bananas$/) { log 'from step' }
        end

        it 'includes the output from the step in the json data' do
          expect(load_normalised_json(@out)).to eq JSON.parse(%(
              [{"id": "banana-party",
                "uri": "spec.feature",
                "keyword": "Feature",
                "name": "Banana party",
                "line": 1,
                "description": "",
                "elements":
                 [{"id": "banana-party;monkey-eats-bananas",
                   "keyword": "Scenario",
                   "name": "Monkey eats bananas",
                   "line": 3,
                   "description": "",
                   "type": "scenario",
                   "steps":
                    [{"keyword": "Given ",
                      "name": "there are bananas",
                      "line": 4,
                      "output": ["from step"],
                      "match": {"location": "spec/cucumber/formatter/json_spec.rb:425"},
                      "result": {"status": "passed",
                                 "duration": 1}}]}]}]))
        end
      end

      describe 'with a background' do
        define_feature <<~FEATURE
          Feature: Banana party

            Background: There are bananas
              Given there are bananas

            Scenario: Monkey eats bananas
              Then the monkey eats bananas

            Scenario: Monkey eats more bananas
              Then the monkey eats more bananas
        FEATURE

        it 'includes the background in the json data each time it is executed' do
          expect(load_normalised_json(@out)).to eq JSON.parse(%(
              [{"id": "banana-party",
                "uri": "spec.feature",
                "keyword": "Feature",
                "name": "Banana party",
                "line": 1,
                "description": "",
                "elements":
                 [{"keyword": "Background",
                   "name": "There are bananas",
                   "line": 3,
                   "description": "",
                   "type": "background",
                   "steps":
                    [{"keyword": "Given ",
                      "name": "there are bananas",
                      "line": 4,
                      "match": {"location": "spec.feature:4"},
                      "result": {"status": "undefined"}}]},
                  {"id": "banana-party;monkey-eats-bananas",
                   "keyword": "Scenario",
                   "name": "Monkey eats bananas",
                   "line": 6,
                   "description": "",
                   "type": "scenario",
                   "steps":
                    [{"keyword": "Then ",
                      "name": "the monkey eats bananas",
                      "line": 7,
                      "match": {"location": "spec.feature:7"},
                      "result": {"status": "undefined"}}]},
                  {"keyword": "Background",
                   "name": "There are bananas",
                   "line": 3,
                   "description": "",
                   "type": "background",
                   "steps":
                    [{"keyword": "Given ",
                      "name": "there are bananas",
                      "line": 4,
                      "match": {"location": "spec.feature:4"},
                      "result": {"status": "undefined"}}]},
                  {"id": "banana-party;monkey-eats-more-bananas",
                   "keyword": "Scenario",
                   "name": "Monkey eats more bananas",
                   "line": 9,
                   "description": "",
                   "type": "scenario",
                   "steps":
                    [{"keyword": "Then ",
                      "name": "the monkey eats more bananas",
                      "line": 10,
                      "match": {"location": "spec.feature:10"},
                      "result": {"status": "undefined"}}]}]}]))
        end
      end

      describe 'with a scenario with a step that embeds data directly' do
        define_feature <<~FEATURE
          Feature: Banana party

            Scenario: Monkey eats bananas
              Given there are bananas
        FEATURE

        define_steps do
          Given(/^there are bananas$/) { attach('YWJj', 'mime-type;base64') }
        end

        it 'includes the data from the step in the json data' do
          expect(load_normalised_json(@out)).to eq JSON.parse(%(
              [{"id": "banana-party",
                "uri": "spec.feature",
                "keyword": "Feature",
                "name": "Banana party",
                "line": 1,
                "description": "",
                "elements":
                 [{"id": "banana-party;monkey-eats-bananas",
                   "keyword": "Scenario",
                   "name": "Monkey eats bananas",
                   "line": 3,
                   "description": "",
                   "type": "scenario",
                   "steps":
                    [{"keyword": "Given ",
                      "name": "there are bananas",
                      "line": 4,
                      "embeddings": [{"mime_type": "mime-type",
                                      "data": "YWJj"}],
                      "match": {"location": "spec/cucumber/formatter/json_spec.rb:535"},
                      "result": {"status": "passed",
                                 "duration": 1}}]}]}]))
        end
      end

      describe 'with a scenario with a step that embeds a file' do
        define_feature <<~FEATURE
          Feature: Banana party

            Scenario: Monkey eats bananas
              Given there are bananas
        FEATURE

        define_steps do
          Given(/^there are bananas$/) do
            RSpec::Mocks.allow_message(File, :file?) { true }
            RSpec::Mocks.allow_message(File, :read) { 'foo' }
            attach('out/snapshot.jpeg', 'image/png')
          end
        end

        it 'includes the file content in the json data' do
          expect(load_normalised_json(@out)).to eq JSON.parse(%(
              [{"id": "banana-party",
                "uri": "spec.feature",
                "keyword": "Feature",
                "name": "Banana party",
                "line": 1,
                "description": "",
                "elements":
                 [{"id": "banana-party;monkey-eats-bananas",
                   "keyword": "Scenario",
                   "name": "Monkey eats bananas",
                   "line": 3,
                   "description": "",
                   "type": "scenario",
                   "steps":
                    [{"keyword": "Given ",
                      "name": "there are bananas",
                      "line": 4,
                      "embeddings": [{"mime_type": "image/png",
                                      "data": "Zm9v"}],
                      "match": {"location": "spec/cucumber/formatter/json_spec.rb:574"},
                      "result": {"status": "passed",
                                 "duration": 1}}]}]}]))
        end
      end

      describe 'with a scenario with hooks' do
        define_feature <<~FEATURE
          Feature: Banana party

            Scenario: Monkey eats bananas
              Given there are bananas
        FEATURE

        define_steps do
          Before() {}
          Before() {}
          After() {}
          After() {}
          AfterStep() {}
          AfterStep() {}
          Around() { |_scenario, block| block.call }
          Given(/^there are bananas$/) {}
        end

        it 'includes all hooks except the around hook in the json data' do
          expect(load_normalised_json(@out)).to eq JSON.parse(%(
              [{"id": "banana-party",
                "uri": "spec.feature",
                "keyword": "Feature",
                "name": "Banana party",
                "line": 1,
                "description": "",
                "elements":
                 [{"id": "banana-party;monkey-eats-bananas",
                   "keyword": "Scenario",
                   "name": "Monkey eats bananas",
                   "line": 3,
                   "description": "",
                   "type": "scenario",
                   "before":
                    [{"match": {"location": "spec/cucumber/formatter/json_spec.rb:617"},
                      "result": {"status": "passed",
                                 "duration": 1}},
                     {"match": {"location": "spec/cucumber/formatter/json_spec.rb:618"},
                      "result": {"status": "passed",
                                 "duration": 1}}],
                   "steps":
                    [{"keyword": "Given ",
                      "name": "there are bananas",
                      "line": 4,
                      "match": {"location": "spec/cucumber/formatter/json_spec.rb:624"},
                      "result": {"status": "passed",
                                 "duration": 1},
                      "after":
                       [{"match": {"location": "spec/cucumber/formatter/json_spec.rb:621"},
                         "result": {"status": "passed",
                                    "duration": 1}},
                        {"match": {"location": "spec/cucumber/formatter/json_spec.rb:622"},
                         "result": {"status": "passed",
                                    "duration": 1}}]}],
                   "after":
                    [{"match": {"location": "spec/cucumber/formatter/json_spec.rb:620"},
                      "result": {"status": "passed",
                                 "duration": 1}},
                     {"match": {"location": "spec/cucumber/formatter/json_spec.rb:619"},
                      "result": {"status": "passed",
                                 "duration": 1}}]}]}]))
        end
      end

      describe 'with a scenario when only an around hook is failing' do
        define_feature <<~FEATURE
          Feature: Banana party

            Scenario: Monkey eats bananas
              Given there are bananas
        FEATURE

        define_steps do
          Around() do |_scenario, block|
            block.call
            raise 'error'
          end
          Given(/^there are bananas$/) {}
        end

        it 'includes the around hook result in the json data' do
          expect(load_normalised_json(@out)).to eq JSON.parse(%{
              [{"id": "banana-party",
                "uri": "spec.feature",
                "keyword": "Feature",
                "name": "Banana party",
                "line": 1,
                "description": "",
                "elements":
                 [{"id": "banana-party;monkey-eats-bananas",
                   "keyword": "Scenario",
                   "name": "Monkey eats bananas",
                   "line": 3,
                   "description": "",
                   "type": "scenario",
                   "steps":
                    [{"keyword": "Given ",
                      "name": "there are bananas",
                      "line": 4,
                      "match": {"location": "spec/cucumber/formatter/json_spec.rb:686"},
                      "result": {"status": "passed",
                                 "duration": 1}}],
                   "around":
                    [{"match": {"location": "unknown_hook_location:1"},
                      "result": {"status": "failed",
                                 "error_message": "error (RuntimeError)\\n./spec/cucumber/formatter/json_spec.rb:684:in `Around'",
                                 "duration": 1}}]}]}]})
        end
      end

      describe 'with a scenario with a step with a data table' do
        define_feature <<~FEATURE
          Feature: Banana party

            Scenario: Monkey eats bananas
              Given there are bananas
                | aa | bb |
                | cc | dd |
        FEATURE

        define_steps do
          Given(/^there are bananas$/) { |s| s }
        end

        it 'includes the doc string in the json data' do
          expect(load_normalised_json(@out)).to eq JSON.parse(%(
              [{"id": "banana-party",
                "uri": "spec.feature",
                "keyword": "Feature",
                "name": "Banana party",
                "line": 1,
                "description": "",
                "elements":
                 [{"id": "banana-party;monkey-eats-bananas",
                   "keyword": "Scenario",
                   "name": "Monkey eats bananas",
                   "line": 3,
                   "description": "",
                   "type": "scenario",
                   "steps":
                     [{"keyword": "Given ",
                       "name": "there are bananas",
                       "line": 4,
                       "rows":
                         [{"cells": ["aa", "bb"]},
                          {"cells": ["cc", "dd"]}],
                       "match": {"location": "spec/cucumber/formatter/json_spec.rb:730"},
                       "result": {"status": "passed",
                                  "duration": 1}}]}]}]))
        end
      end

      def load_normalised_json(out)
        normalise_json(JSON.parse(out.string))
      end

      def normalise_json(json)
        # make sure duration was captured (should be >= 0)
        # then set it to what is "expected" since duration is dynamic
        json.each do |feature|
          elements = feature.fetch('elements') { [] }
          elements.each do |scenario|
            %w[steps before after around].each do |type|
              next unless scenario[type]

              scenario[type].each do |step_or_hook|
                normalise_json_step_or_hook(step_or_hook)
                next unless step_or_hook['after']

                step_or_hook['after'].each do |hook|
                  normalise_json_step_or_hook(hook)
                end
              end
            end
          end
        end
      end

      def normalise_json_step_or_hook(step_or_hook)
        return unless step_or_hook['result'] && step_or_hook['result']['duration']

        expect(step_or_hook['result']['duration']).to be >= 0
        step_or_hook['result']['duration'] = 1
      end
    end
  end
end
