require_relative '../../../lib/knapsack_pro/formatters/time_tracker'

describe KnapsackPro::Adapters::RSpecAdapter do
  it 'backwards compatibility with knapsack gem old rspec adapter name' do
    expect(KnapsackPro::Adapters::RspecAdapter.new).to be_kind_of(described_class)
  end

  it do
    expect(described_class::TEST_DIR_PATTERN).to eq 'spec/**{,/*/**}/*_spec.rb'
  end

  context do
    before { expect(::RSpec).to receive(:configure).at_least(:once) }
    it_behaves_like 'adapter'
  end

  describe '.split_by_test_cases_enabled?' do
    subject { described_class.split_by_test_cases_enabled? }

    before do
      expect(KnapsackPro::Config::Env).to receive(:rspec_split_by_test_examples?).and_return(rspec_split_by_test_examples_enabled)
    end

    context 'when the RSpec split by test examples is enabled' do
      let(:rspec_split_by_test_examples_enabled) { true }

      it { expect(subject).to be true }

      context 'when the RSpec version is < 3.3.0' do
        before do
          stub_const('RSpec::Core::Version::STRING', '3.2.0')
        end

        it do
          expect { subject }.to raise_error RuntimeError, 'RSpec >= 3.3.0 is required to split test files by test examples. Learn more: https://knapsackpro.com/perma/ruby/split-by-test-examples'
        end
      end
    end

    context 'when the RSpec split by test examples is disabled' do
      let(:rspec_split_by_test_examples_enabled) { false }

      it { expect(subject).to be false }
    end
  end

  describe '.test_file_cases_for' do
    let(:slow_test_files) do
      [
        '1_spec.rb',
        '2_spec.rb',
        '3_spec.rb',
        '4_spec.rb',
        '5_spec.rb',
      ]
    end

    subject { described_class.test_file_cases_for(slow_test_files) }

    before do
      logger = instance_double(Logger)
      expect(KnapsackPro).to receive(:logger).and_return(logger)
      expect(logger).to receive(:info).with("Generating RSpec test examples JSON report for slow test files to prepare it to be split by test examples (by individual test cases). Thanks to that, a single slow test file can be split across parallel CI nodes. Analyzing 5 slow test files.")

      cmd = 'RACK_ENV=test RAILS_ENV=test bundle exec rake knapsack_pro:rspec_test_example_detector'
      expect(Kernel).to receive(:system).with(cmd).and_return(cmd_result)
    end

    context 'when the rake task to detect RSpec test examples succeeded' do
      let(:cmd_result) { true }

      it 'returns test example paths for slow test files' do
        rspec_test_example_detector = instance_double(KnapsackPro::TestCaseDetectors::RSpecTestExampleDetector)
        expect(KnapsackPro::TestCaseDetectors::RSpecTestExampleDetector).to receive(:new).and_return(rspec_test_example_detector)

        test_file_example_paths = double
        expect(rspec_test_example_detector).to receive(:test_file_example_paths).and_return(test_file_example_paths)

        expect(subject).to eq test_file_example_paths
      end
    end

    context 'when the rake task to detect RSpec test examples failed' do
      let(:cmd_result) { false }

      it do
        expect { subject }.to raise_error(RuntimeError, 'Could not generate JSON report for RSpec. Rake task failed when running RACK_ENV=test RAILS_ENV=test bundle exec rake knapsack_pro:rspec_test_example_detector')
      end
    end
  end

  describe '.ensure_no_tag_option_when_rspec_split_by_test_examples_enabled!' do
    let(:cli_args) { double }

    subject { described_class.ensure_no_tag_option_when_rspec_split_by_test_examples_enabled!(cli_args) }

    before do
      expect(KnapsackPro::Config::Env).to receive(:rspec_split_by_test_examples?).and_return(rspec_split_by_test_examples_enabled)
    end

    context 'when RSpec split by test examples enabled' do
      let(:rspec_split_by_test_examples_enabled) { true }

      before do
        expect(described_class).to receive(:has_tag_option?).with(cli_args).and_return(has_tag_option)
      end

      context 'when RSpec tag option is provided' do
        let(:has_tag_option) { true }

        it do
          expect { subject }.to raise_error(/It is not allowed to use the RSpec tag option together with the RSpec split by test examples feature/)
        end
      end

      context 'when RSpec tag option is not provided' do
        let(:has_tag_option) { false }

        it 'does nothing' do
          expect(subject).to be_nil
        end
      end
    end

    context 'when RSpec split by test examples disabled' do
      let(:rspec_split_by_test_examples_enabled) { false }

      it 'does nothing' do
        expect(subject).to be_nil
      end
    end
  end

  describe '.has_tag_option?' do
    subject { described_class.has_tag_option?(cli_args) }

    context 'when tag option is provided as -t' do
      let(:cli_args) { ['-t', 'mytag'] }

      it { expect(subject).to be true }
    end

    context 'when tag option is provided as --tag' do
      let(:cli_args) { ['--tag', 'mytag'] }

      it { expect(subject).to be true }
    end

    context 'when tag option is provided without delimiter' do
      let(:cli_args) { ['-tmytag'] }

      it { expect(subject).to be true }
    end

    context 'when tag option is not provided' do
      let(:cli_args) { ['--fake', 'value'] }

      it { expect(subject).to be false }
    end
  end

  describe '.has_format_option?' do
    subject { described_class.has_format_option?(cli_args) }

    context 'when format option is provided as -f' do
      let(:cli_args) { ['-f', 'documentation'] }

      it { expect(subject).to be true }
    end

    context 'when format option is provided as --format' do
      let(:cli_args) { ['--format', 'documentation'] }

      it { expect(subject).to be true }
    end

    context 'when format option is provided without delimiter' do
      let(:cli_args) { ['-fd'] }

      it { expect(subject).to be true }
    end

    context 'when format option is not provided' do
      let(:cli_args) { ['--fake', 'value'] }

      it { expect(subject).to be false }
    end
  end

  describe '.has_require_rails_helper_option?' do
    subject { described_class.has_require_rails_helper_option?(cli_args) }

    context 'when require option is provided as -r' do
      let(:cli_args) { ['-r', 'rails_helper'] }

      it { expect(subject).to be true }
    end

    context 'when require option is provided as --require' do
      let(:cli_args) { ['--require', 'rails_helper'] }

      it { expect(subject).to be true }
    end

    context 'when require option is provided without delimiter' do
      let(:cli_args) { ['-rrails_helper'] }

      it { expect(subject).to be true }
    end

    context 'when require option is not provided' do
      let(:cli_args) { ['--fake', 'value'] }

      it { expect(subject).to be false }
    end
  end

  describe '.rails_helper_exists?' do
    subject { described_class.rails_helper_exists?(test_dir) }

    let(:test_dir) { 'spec_fake' }

    context 'when rails_helper exists' do
      before do
        File.open("#{test_dir}/rails_helper.rb", 'w')
      end

      after do
        FileUtils.rm("#{test_dir}/rails_helper.rb")
      end

      it { expect(subject).to be true }
    end

    context 'when rails_helper does not exist' do
      before do
        FileUtils.rm_f("#{test_dir}/rails_helper.rb")
      end

      it { expect(subject).to be false }
    end
  end

  describe '.order_option' do
    subject { described_class.order_option(cli_args) }

    context "when order is 'defined'" do
      let(:cli_args) { ['--order', 'defined'] }

      it { expect(subject).to eq 'defined' }
    end

    context "when order is 'recently-modified'" do
      let(:cli_args) { ['--order', 'recently-modified'] }

      it { expect(subject).to eq 'recently-modified' }
    end

    context "when order is 'rand'" do
      let(:cli_args) { ['--order', 'rand'] }

      it { expect(subject).to eq 'rand' }

      context 'with the seed' do
        let(:cli_args) { ['--order', 'rand:123456'] }

        it { expect(subject).to eq 'rand:123456' }
      end
    end

    context "when order is 'random'" do
      let(:cli_args) { ['--order', 'random'] }

      it { expect(subject).to eq 'random' }

      context 'with the seed' do
        let(:cli_args) { ['--order', 'random:123456'] }

        it { expect(subject).to eq 'random:123456' }
      end
    end

    context 'when some custom order is specified' do
      let(:cli_args) { ['--order', 'some-custom-order'] }

      it { expect(subject).to eq 'some-custom-order' }
    end

    context "when the seed is given with the --seed command" do
      let(:cli_args) { ['--seed', '123456'] }

      it { expect(subject).to eq 'rand:123456' }
    end
  end

  describe '.file_path_for' do
    let(:current_example) { ::RSpec.describe.example }

    subject { described_class.file_path_for(current_example) }

    context "when id ends in _spec.rb" do
      it "returns the first part of the id" do
        allow(current_example).to receive(:id).and_return("./foo_spec.rb[1:1]")

        expect(subject).to eq('./foo_spec.rb')
      end
    end

    context "when id does not end in _spec.rb" do
      it "returns the file_path" do
        allow(current_example).to receive(:id).and_return("./foo.rb")
        allow(current_example).to receive(:metadata).and_return(file_path: "./foo_spec.rb")

        expect(subject).to eq('./foo_spec.rb')
      end
    end

    context "when id and file_path do not end in _spec.rb" do
      it "returns the example_group's file_path" do
        allow(current_example).to receive(:id).and_return("./foo.rb")
        allow(current_example).to receive(:metadata).and_return(
          file_path: "./foo.rb", example_group: { file_path: "./foo_spec.rb" }
        )

        expect(subject).to eq('./foo_spec.rb')
      end
    end

    context "when id, file_path, and example_group's file_path do not end in _spec.rb" do
      it "returns the top_level_group's file_path" do
        allow(current_example).to receive(:id).and_return("./foo.rb")
        allow(current_example).to receive(:metadata).and_return(
          file_path: "./foo.rb",
          example_group: {
            file_path: "./foo.rb",
            parent_example_group: {
              file_path: "./foo_spec.rb",
            }
          }
        )

        expect(subject).to eq('./foo_spec.rb')
      end
    end

    context "when id, file_path, example_group's, and top_level_group's file_path do not end in _spec.rb" do
      it "returns empty string" do
        allow(current_example).to receive(:id).and_return("./foo.rb")
        allow(current_example).to receive(:metadata).and_return(
          file_path: "./foo.rb",
          example_group: {
            file_path: "./foo.rb",
            parent_example_group: {
              file_path: "./foo.rb",
            }
          }
        )

        expect(subject).to eq('')
      end
    end

    context "when id does not end in .feature (nor _spec.rb)" do
      it "returns the file_path" do
        allow(current_example).to receive(:id).and_return("./foo.rb")
        allow(current_example).to receive(:metadata).and_return(file_path: "./foo.feature")

        expect(subject).to eq("./foo.feature")
      end
    end
  end

  describe 'bind methods' do
    let(:config) { double }

    describe '#bind_time_tracker' do
      let(:current_example) { double(metadata: {}) }

      context "when the example's metadata has :focus tag AND RSpec inclusion rule includes :focus" do
        let(:current_example) { double(metadata: { focus: true }) }
        let(:test_path) { 'spec/a_spec.rb' }

        it do
          expect(config).to receive(:around).with(:each).and_yield(current_example)
          expect(::RSpec).to receive(:configure).and_yield(config)

          expect(described_class).to receive(:file_path_for).with(current_example).and_return(test_path)

          expect(described_class).to receive_message_chain(:rspec_configuration, :filter, :rules, :[]).with(:focus).and_return(true)

          expect {
            subject.bind_time_tracker
          }.to raise_error /Knapsack Pro found an example tagged with focus in spec\/a_spec\.rb/i
        end
      end

      context 'with no focus' do
        it 'records time for current test path' do
          expect(config).to receive(:around).with(:each).and_yield(current_example)
          expect(config).to receive(:append_after).with(:suite)
          expect(::RSpec).to receive(:configure).at_least(1).and_yield(config)

          expect(current_example).to receive(:run)

          subject.bind_time_tracker
        end
      end
    end

    describe '#bind_save_report' do
      it do
        expect(config).to receive(:after).with(:suite).and_yield
        expect(::RSpec).to receive(:configure).and_yield(config)

        time_tracker = instance_double(KnapsackPro::Formatters::TimeTracker)
        times = [{ path: "foo_spec.rb", time_execution: 1.0 }]
        expect(time_tracker).to receive(:batch).and_return(times)
        expect(KnapsackPro::Formatters::TimeTrackerFetcher).to receive(:call).and_return(time_tracker)
        expect(KnapsackPro::Report).to receive(:save).with(times)

        subject.bind_save_report
      end
    end
  end
end