# frozen_string_literal: true

require 'semver_dialects'
require 'yaml'

RSpec.describe SemverDialects do # rubocop:todo Metrics/BlockLength
  describe '.parse_version' do # rubocop:todo Metrics/BlockLength
    subject { described_class.parse_version(typ, version) }
    let(:typ) { 'gem' }
    let(:version) { '1.2.3' }

    context 'with a supported syntax and valid version' do
      using RSpec::Parameterized::TableSyntax

      where(:typ, :version, :expected_class) do
        'maven' | '1.2.3' | SemverDialects::Maven::Version
        'npm' | '1.2.3' | SemverDialects::Semver2::Version
        'go' | '1.2.3' | SemverDialects::Semver2::Version
        'pypi' | '1.2.3' | SemverDialects::SemanticVersion
        'gem' | '1.2.3.rc.1' | Gem::Version
        'packagist' | '1.2.3' | SemverDialects::SemanticVersion
        'conan' | '1.2.3' | SemverDialects::SemanticVersion
        'swift' | '1.2.3' | SemverDialects::Semver2::Version
        'pub' | '1.2.3' | SemverDialects::Semver2::Version
      end

      with_them do
        it { is_expected.to be_instance_of(expected_class) }

        context 'to_s' do
          it 'matches the input string' do
            expect(subject.to_s).to eq(version)
          end
        end
      end
    end

    context 'with supported package types', :aggregate_failures do
      described_class.supported_package_types.each do |pkg_type|
        it 'handles the package type' do
          expect { described_class.parse_version(pkg_type, '1.0.0') }
            .not_to raise_error(SemverDialects::UnsupportedPackageTypeError)
        end
      end
    end

    context 'with a supported syntax and an invalid version' do
      using RSpec::Parameterized::TableSyntax

      let(:version) { '?' }

      where(:typ) do
        # Language package types
        'conan'     | nil
        'gem'       | nil
        'go'        | nil
        'maven'     | nil
        'npm'       | nil
        'packagist' | nil
        'pypi'      | nil
        'swift'     | nil
        'pub'       | nil

        # OS package types
        'deb'       | nil
        'rpm'       | nil
      end

      it 'raises an exception' do
        expect { subject }.to raise_error(SemverDialects::InvalidVersionError)
      end
    end

    context 'with nil or empty raw_ver' do
      described_class.supported_package_types.each do |pkg_type|
        context "with type '#{pkg_type}'" do
          it 'raises InvalidVersionError for nil' do
            expect { described_class.parse_version(pkg_type, nil) }
              .to raise_error(SemverDialects::InvalidVersionError)
          end

          it "raises InvalidVersionError for ''" do
            expect { described_class.parse_version(pkg_type, '') }
              .to raise_error(SemverDialects::InvalidVersionError)
          end
        end
      end
    end

    context 'when a dialect parser raises TypeError' do
      it 'raises InvalidVersionError' do
        expect { described_class.parse_version('go', 123) }
          .to raise_error(SemverDialects::InvalidVersionError)
      end
    end

    context 'when a dialect parser raises NoMethodError' do
      it 'raises InvalidVersionError' do
        obj = Object.new
        expect { described_class.parse_version('nuget', obj) }
          .to raise_error(SemverDialects::InvalidVersionError)
      end
    end

    context 'when the package type is not supported' do
      let(:typ) { 'unsupported' }
      let(:supported_types_string) { described_class.supported_package_types.join(', ') }
      let(:expected_message) { "unsupported package type '#{typ}'. Supported types are: #{supported_types_string}" }

      it 'raises an exception' do
        expect { subject }
          .to raise_error(SemverDialects::UnsupportedPackageTypeError, expected_message)
      end
    end

    context 'when supplied version is loose' do
      using RSpec::Parameterized::TableSyntax
      subject { described_class.parse_version(typ, loose_version).to_s }

      let(:expectation_tuples) do
        [
          [' 1.2.3 ', '1.2.3'],
          [' 1.2.3-4 ', '1.2.3-4'],
          [' 1.2.3-pre ', '1.2.3-pre'],
          ['  =v1.2.3   ', '1.2.3'],
          [' v1.2.3 ', '1.2.3'],
          ["\t1.2.3", '1.2.3']
        ]
      end

      context 'with npm package type' do
        let(:typ) { 'npm' }

        where(:loose_version, :expected_version) { expectation_tuples }

        with_them do
          it 'gets cleaned up' do
            expect(subject).to eq(expected_version)
          end
        end
      end

      context 'with non-npm package type' do
        let(:typ) { 'cargo' }

        where(:loose_version, :expected_version) { expectation_tuples }

        with_them do
          it 'errors out' do
            expect { subject }.to raise_error(SemverDialects::IncompleteScanError)
          end
        end
      end
    end
  end
end

RSpec.describe SemverDialects do # rubocop:todo Metrics/BlockLength
  describe '.version_satisfies?' do # rubocop:todo Metrics/BlockLength
    shared_examples 'version satisfies constraint' do |typ|
      it 'returns the expected result' do
        test_cases.each do |raw_version, raw_constraint, expected|
          expect(described_class.version_satisfies?(typ, raw_version, raw_constraint))
            .to eql(expected), "Expected type '#{typ}' with version '#{raw_version}' to\
            'be #{expected} for constraint '#{raw_constraint}'"
        end
      end
    end

    context 'with range for' do
      root_dir = File.expand_path('../..', __dir__)
      input_dir = File.join(root_dir, 'expectation', 'version_sat')
      input_paths = Dir.glob(File.join(input_dir, '*.yaml'))

      input_paths.each do |input_path|
        basename = File.basename(input_path, '.*')

        context basename do
          examples = YAML.load_stream(File.read(input_path))
          examples.each do |example|
            example['status']
            pkg_name = example['package']['name']
            pkg_type = example['package']['type']
            range = example['range']
            included_versions = example['versions']['in']
            excluded_versions = example['versions']['out']
            example_name = "#{pkg_name} '#{range}'"

            # Each YAML document combines a version range, included versions, and excluded versions.
            #
            # If the "status" field of the YAML document is "resolved" or undefined,
            # then create a context for the range, and an example for each included and excluded version.
            # The included and excluded versions have been checked and they're correct,
            # and the corresponding examples should pass.
            #
            # If "status" has any other value, create a pending context.
            # - "needs triage" indicates that the included and excluded versions need to be checked.
            # - "confirmed" indicates that included and excluded versions are correct.
            #   However, the implementation still needs to be fixed.
            #
            case example['status']
            when 'resolved', nil
              context example_name do
                included_versions&.each do |version|
                  it "includes #{version}" do
                    expect(described_class.version_satisfies?(pkg_type, version, range)).to be true
                  end
                end

                excluded_versions&.each do |version|
                  it "excludes #{version}" do
                    expect(described_class.version_satisfies?(pkg_type, version, range)).to be false
                  end
                end
              end
            else
              pending example_name
            end
          end
        end
      end
    end

    context 'when the package type is supported' do
      it 'returns the expected result for the given input' do
        [
          ['pypi',      '1.0.0', '<10.0.0', true],
          ['pypi',      '2.4.0-rc0', '==2.4.0-rc0,==2.4.0-rc1,==2.4.0-rc2,==2.4.0-rc3,==2.4.0-rc4', false],
          ['pypi',      '1.0.0',  '>10.0.0',   false],
          ['packagist', '1.0.0',  '<10.0.0',   true],
          ['packagist', '1.0.0',  '>10.0.0',   false],
          ['maven',     '1.0.0',  '<10.0.0',   true],
          ['maven',     '1.0.0',  '>10.0.0',   true],
          ['maven',     '1.0.0',  '(,10.0.0)', true],
          ['maven',     '1.0.0',  '(10.0.0,)', false],
          ['gem',       '1.0.0',  '<10.0.0',   true],
          ['gem',       '1.0.0',  '>10.0.0',   false],
          ['go',        'v1.0.0', '<v10.0.0',  true],
          ['go',        'v1.0.0', '>v10.0.0',  false],
          ['npm',       '1.0.0',  '<10.0.0',   true],
          ['npm',       '1.0.0',  '>10.0.0',   false],
          ['nuget',     '1.0.0',  '<10.0.0',   true],
          ['nuget',     '1.0.0',  '>10.0.0',   true],
          ['nuget',     '1.0.0',  '(,10.0.0)', true],
          ['nuget',     '1.7.5',  '[1.7.5,1.7.5)', false],
          ['nuget',     '1.8.5',  '[1.7.5,1.7.5)', false],
          ['nuget',     '1.0.0',  '(10.0.0,)', false],
          ['conan',     '1.0.0',  '<10.0.0',   true],
          ['conan',     '1.0.0',  '>10.0.0',   false],
          ['swift',     '1.0.0',  '<10.0.0',   true],
          ['swift',     '1.0.0',  '>10.0.0',   false],
          ['pub',       '1.0.0',  '<10.0.0',   true],
          ['pub',       '1.0.0',  '>10.0.0',   false]
        ].each do |typ, raw_version, raw_constraint, expected|
          expect(described_class.version_satisfies?(typ, raw_version, raw_constraint))
            .to eql(expected), "Expected type '#{typ}' with version '#{raw_version}' to \
            'be #{expected} for constraint '#{raw_constraint}'"
        end
      end
    end

    context 'when OS pkg' do
      context 'with deb purl type' do
        it_behaves_like 'version satisfies constraint', 'deb' do
          let(:test_cases) do
            [
              # RedHat
              ['7.4.629-3',            '<7.4.629-5',            true],
              ['7.4.622-1',            '<7.4.629-1',            true],
              ['6.0-4.el6.x86_64',     '<6.0-5.el6.x86_64',     true],
              ['6.0-4.el6.x86_64',     '<6.1-3.el6.x86_64',     true],
              ['7.0-4.el6.x86_64',     '<6.1-3.el6.x86_64',     false],
              # Debian
              ['2:7.4.052-1ubuntu3',   '<2:7.4.052-1ubuntu3.1', true],
              ['2:7.4.052-1ubuntu2',   '<2:7.4.052-1ubuntu3',   true],
              ['2:7.4.052-1',          '<2:7.4.052-1ubuntu3',   true],
              ['2:7.4.052',            '<2:7.4.052-1',          true],
              ['1:7.4.052',            '<2:7.4.052',            true],
              ['1:7.4.052',            '<7.4.052',              false],
              ['2:7.4.052-1ubuntu3.2', '<2:7.4.052-1ubuntu3.1', false],
              ['2:7.4.052-1ubuntu3.1', '<2:7.4.052-1ubuntu3', false],
              ['2:7.4.052-1ubuntu3',   '<2:7.4.052-1ubuntu2',   false],
              ['2:7.4.052-1ubuntu1',   '<2:7.4.052-1',          false],
              ['2:6.0-9ubuntu1.4',     '<2:6.0-9ubuntu1.5',     true],
              ['2:7.4.052-1ubuntu',    '<2:7.4.052-1',          false],
              ['6.4.052',              '<7.4.052',              true],
              ['6.4.052',              '<6.5.052',              true],
              ['6.4.052',              '<6.4.053',              true],
              ['1ubuntu1',             '<1ubuntu3.1',           true],
              ['1',                    '<1ubuntu1',             true],
              ['7.4.027',              '<7.4.052',              true]
            ]
          end
        end
      end

      context 'with rpm purl type' do
        it_behaves_like 'version satisfies constraint', 'rpm' do
          let(:test_cases) do
            [
              # oracle
              ['2.9.1-6.0.1.el7_2.3', '<2.9.1-6.el7_2.3', false],
              ['3.10.0-327.28.3.el7', '<3.10.0-327.el7', false],
              ['3.14.3-23.3.el6_8', '<3.14.3-23.el6_7', false],
              ['2.23.2-22.el7_1', '<2.23.2-22.el7_1.1', true]
            ]
          end
        end
      end

      context 'with apk purl type' do
        it_behaves_like 'version satisfies constraint', 'apk' do
          let(:test_cases) do
            [
              ['1.2.3', '<1.2.2', false],
              ['1.2.0a', '<1.2.0', false],
              ['1.2.3_alpha', '<1.2.3', true],
              ['1.2.3_cvs', '<1.2.3', false],
              ['1.2.3-r', '<1.2.3', false]
            ]
          end
        end
      end
    end

    context 'when the package type is not supported' do
      let(:typ) { 'unsupported' }
      let(:supported_types_string) { described_class.supported_package_types.join(', ') }
      let(:expected_message) { "unsupported package type '#{typ}'. Supported types are: #{supported_types_string}" }

      it 'returns an exception' do
        expect { described_class.version_satisfies?(typ, '1.0.0', '<10.0.0') }
          .to raise_error(SemverDialects::UnsupportedPackageTypeError, expected_message)
      end
    end

    context 'with an invalid constraint' do
      context 'no operator' do
        it 'returns an exception' do
          expect { described_class.version_satisfies?('gem', '1.0.0', 'invalid-constraint') }
            .to raise_error(SemverDialects::InvalidConstraintError, "invalid constraint '=invalid-constraint'")
        end
      end

      context 'valid operator but invalid version' do
        it 'returns an exception' do
          expect { described_class.version_satisfies?('gem', '1.0.0', '<invalid-version') }
            .to raise_error(SemverDialects::InvalidConstraintError, "invalid constraint '<invalid-version'")
        end
      end

      context 'disjunction with invalid version' do
        it 'returns an exception' do
          expect { described_class.version_satisfies?('gem', '1.0.0', '>=1.0.0||<invalid-version') }
            .to raise_error(SemverDialects::InvalidConstraintError, "invalid constraint '<invalid-version'")
        end
      end
    end

    context 'with an invalid version' do
      it 'returns an exception' do
        expect { described_class.version_satisfies?('gem', 'invalid-version', '<1.0.0') }
          .to raise_error(SemverDialects::InvalidVersionError, "invalid version 'invalid-version'")
      end
    end

    context 'with an unsupported version' do
      it 'returns an exception' do
        expect { described_class.version_satisfies?('apk', '4.09-r1', '<4.5.14') }
          .to raise_error(SemverDialects::UnsupportedVersionError, "unsupported version '4.09-r1'")
      end
    end
  end
end
