/*
 * This file is part of dependency-check-core.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Copyright (c) 2013 Jeremy Long. All Rights Reserved.
 */
package org.owasp.dependencycheck.dependency;


import io.github.jeremylong.openvulnerability.client.nvd.CvssV2;
import io.github.jeremylong.openvulnerability.client.nvd.CvssV2Data;
import io.github.jeremylong.openvulnerability.client.nvd.CvssV3;
import io.github.jeremylong.openvulnerability.client.nvd.CvssV3Data;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import org.junit.Test;
import org.owasp.dependencycheck.BaseTest;
import us.springett.parsers.cpe.exceptions.CpeValidationException;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.TreeSet;

/**
 * @author Jens Hausherr, Hans Aikema
 */
public class VulnerabilityTest extends BaseTest {

    /**
     * Test of addVulnerableSoftware method, of class VulnerableSoftware.
     * @throws CpeValidationException
     */
    @Test
    public void testDuplicateVersions() throws CpeValidationException {
        Vulnerability obj = new Vulnerability();
        VulnerableSoftwareBuilder builder = new VulnerableSoftwareBuilder();
        obj.addVulnerableSoftware(builder.vendor("owasp").product("dependency-check").version("3.0.0").build());
        obj.addVulnerableSoftware(builder.vendor("owasp").product("dependency-check").version("4.0.0").build());
        obj.addVulnerableSoftware(builder.vendor("owasp").product("dependency-check").version("3.0.0").build());

        assertEquals(2, obj.getVulnerableSoftware().size());
    }

    /**
     * Test of compareTo
     */
    @Test
    public void compareTo_proper_sorting() {
        // vulnerabilities take a name in reverse alphabetical order of expected sorting sequence
        // this way we ensure that the validated sorting was indeed due to the decreasing order of
        // severity and not by coincidence due to equal severity + alphabetical order on name.
        // as equals() takes only name for comparison we need to make names unique for an easy equals()
        // comparison of the result

        Vulnerability cvssV3OnlyCritHigh = new Vulnerability("Z most severe");
        cvssV3OnlyCritHigh.setCvssV3(createCvssV3(10.0, "CRITICAL"));

        Vulnerability cvssV3Only90 = new Vulnerability("Y 2nd most severe");
        cvssV3Only90.setCvssV3(createCvssV3( 9.0, "CRITICAL"));

        Vulnerability unscoredCritical = new Vulnerability("X 3rd most severe");
        unscoredCritical.setUnscoredSeverity("Critical");

        Vulnerability unscoredAssumedCritical = new Vulnerability("W 4th most severe");
        unscoredAssumedCritical.setUnscoredSeverity("Foobar");

        Vulnerability cvssV2Only10_0 = new Vulnerability("V 5th most severe");
        cvssV2Only10_0.setCvssV2(createCvssV2(10.0, "HIGH"));

        Vulnerability cvssV2Only99 = new Vulnerability("U 6th most severe");
        final CvssV2 cvssV2_99 = createCvssV2(9.9f, "HIGH");
        cvssV2Only99.setCvssV2(cvssV2_99);

        Vulnerability cvssV2Only90 = new Vulnerability("T 7th most severe");
        final CvssV2 cvssV2_90 = createCvssV2(9.0, "HIGH");
        cvssV2Only90.setCvssV2(cvssV2_90);

        Vulnerability cvssV3_89v2_90 = new Vulnerability("SA 8th most severe, alphabetical first");
        final CvssV3 cvssV3_89 = createCvssV3(8.9, "HIGH");
        cvssV3_89v2_90.setCvssV3(cvssV3_89);
        cvssV3_89v2_90.setCvssV2(cvssV2_90);

        Vulnerability cvssV3_89v2_99 = new Vulnerability("SB 8th most severe, alphabetical second");
        cvssV3_89v2_99.setCvssV3(cvssV3_89);
        cvssV3_89v2_99.setCvssV2(cvssV2_99);

        Vulnerability cvssV3_80v2_99 = new Vulnerability("R 9th most severe");
        cvssV3_80v2_99.setCvssV3(createCvssV3(8.0, "HIGH"));
        cvssV3_80v2_99.setCvssV2(cvssV2_99);

        Vulnerability unscoredHigh = new Vulnerability("Q 10th most severe");
        unscoredHigh.setUnscoredSeverity("hIGH");

        Vulnerability v3Medium69 = new Vulnerability("P 11th most severe");
        v3Medium69.setCvssV3(createCvssV3(6.9, "MEDIUM"));

        Vulnerability unscoredMedium = new Vulnerability("O 12th most severe");
        unscoredMedium.setUnscoredSeverity("meDiUm");


        assertTrue("V2 HIGH 9.9 to V2 HIGH 9.0, 9.9 should be most severe", cvssV2Only99.compareTo(cvssV2Only90) < 0);
        assertTrue("V2 HIGH 9.9 to V3 CRIT 9.0 should make V3 9.0 should be most severe to retain the CRITICAL rating",
                   cvssV3Only90.compareTo(cvssV2Only99) < 0);
        assertTrue("V3 CRIT 9.9 to V3 CRIT 9.0, 9.9 should be most severe", cvssV3OnlyCritHigh.compareTo(cvssV3Only90) < 0);
        assertTrue("V3 CRIT 9.0 to V3 HIGH 8.0 V2 HIGH 9.9, V3 9.0 should be most severe",
                   cvssV3Only90.compareTo(cvssV3_80v2_99) < 0);
        assertTrue("CVSS v3 CRITICAL should be smaller (more severe) than unscored critical",
                   cvssV3Only90.compareTo(unscoredCritical) < 0);
        assertTrue("unscored critical should be smaller (more severe) than CVSS v2 HIGH 10.0 should be larger (less severe)",
                   unscoredCritical.compareTo(cvssV2Only10_0) < 0);
        assertTrue("CVSS v3 CRITICAL should be smaller (more severe) than unscored assumed critical",
                   cvssV3Only90.compareTo(unscoredAssumedCritical) < 0);
        assertTrue("Unscored CRITICAL should be smaller (more severe) than unscored assumed critical",
                   unscoredCritical.compareTo(unscoredAssumedCritical) < 0);
        assertTrue("unscored assumed critical should be smaller (more severe) than CVSS v2 HIGH 10.0",
                   unscoredAssumedCritical.compareTo(cvssV2Only10_0) < 0);
        assertTrue("unscored assumed critical should be smaller (more severe) CVSS v2 9.9 v3 8.0 (HIGH)",
                   unscoredAssumedCritical.compareTo(cvssV3_80v2_99) < 0);
        assertTrue("CVSS v3 score should be considered over V2 score; alphabetical sort determines final sequence",
                   cvssV3_89v2_90.compareTo(cvssV3_89v2_99) < 0);
        assertTrue("CVSS v3 medium top-range score should be smaller (more severe) than unscored medium",
                   v3Medium69.compareTo(unscoredMedium) < 0);

        List<Vulnerability> vulns = Arrays.asList(cvssV2Only99, cvssV2Only90, cvssV3Only90, cvssV3OnlyCritHigh, cvssV3_80v2_99,
                                                  cvssV2Only10_0, cvssV3_89v2_90,
                                                  cvssV3_89v2_99, unscoredHigh, v3Medium69, unscoredCritical, unscoredMedium, unscoredAssumedCritical);
        vulns.sort(Vulnerability::compareTo);
        List<String> expectedStartLetters = Arrays.asList("Z", "Y", "X", "W", "V", "U", "T", "SA", "SB", "R", "Q", "P", "O");

        for (int i = 0; i < vulns.size(); i++) {
            assertTrue("Expected start:" + expectedStartLetters.get(i) + " encountered start: " + vulns.get(i).getName()
                                                                                                       .substring(0, 2),
                       vulns.get(i).getName().startsWith(expectedStartLetters.get(i)));
        }
        testSortStabilityForPermutations(vulns);
    }

    private CvssV3 createCvssV3(double score, String severity) {
        CvssV3Data v3Data = new CvssV3Data(CvssV3Data.Version._3_0, null, CvssV3Data.AttackVectorType.NETWORK,
                CvssV3Data.AttackComplexityType.HIGH, CvssV3Data.PrivilegesRequiredType.HIGH,
                CvssV3Data.UserInteractionType.NONE, CvssV3Data.ScopeType.CHANGED,
                CvssV3Data.CiaType.NONE, CvssV3Data.CiaType.NONE, CvssV3Data.CiaType.LOW,
                
                score, CvssV3Data.SeverityType.valueOf(severity), 
                
                CvssV3Data.ExploitCodeMaturityType.PROOF_OF_CONCEPT, CvssV3Data.RemediationLevelType.NOT_DEFINED, 
                CvssV3Data.ConfidenceType.REASONABLE, Double.MAX_VALUE, CvssV3Data.SeverityType.MEDIUM,
                CvssV3Data.CiaRequirementType.NOT_DEFINED, CvssV3Data.CiaRequirementType.NOT_DEFINED,
                CvssV3Data.CiaRequirementType.NOT_DEFINED, CvssV3Data.ModifiedAttackVectorType.ADJACENT_NETWORK,
                CvssV3Data.ModifiedAttackComplexityType.NOT_DEFINED, CvssV3Data.ModifiedPrivilegesRequiredType.NOT_DEFINED,
                CvssV3Data.ModifiedUserInteractionType.NOT_DEFINED, CvssV3Data.ModifiedScopeType.NOT_DEFINED,
                CvssV3Data.ModifiedCiaType.NOT_DEFINED, CvssV3Data.ModifiedCiaType.NOT_DEFINED,
                CvssV3Data.ModifiedCiaType.NOT_DEFINED, Double.MIN_VALUE, CvssV3Data.SeverityType.NONE);
        CvssV3 cvssV3 = new CvssV3(null, CvssV3.Type.PRIMARY, v3Data, null, null);
        return cvssV3;
    }

    
    private CvssV2 createCvssV2(double score, String severity) {
        CvssV2Data v2Data = new CvssV2Data("2.0", severity, CvssV2Data.AccessVectorType.NETWORK, 
                CvssV2Data.AccessComplexityType.MEDIUM, CvssV2Data.AuthenticationType.MULTIPLE, 
                CvssV2Data.CiaType.PARTIAL, CvssV2Data.CiaType.PARTIAL, CvssV2Data.CiaType.PARTIAL, 
                
                score, severity, 
                
                CvssV2Data.ExploitabilityType.UNPROVEN, CvssV2Data.RemediationLevelType.NOT_DEFINED, 
                CvssV2Data.ReportConfidenceType.UNCONFIRMED, 0.0, CvssV2Data.CollateralDamagePotentialType.NOT_DEFINED, 
                CvssV2Data.TargetDistributionType.MEDIUM, CvssV2Data.CiaRequirementType.NOT_DEFINED, 
                CvssV2Data.CiaRequirementType.NOT_DEFINED, CvssV2Data.CiaRequirementType.NOT_DEFINED, 0.0);
        CvssV2 cvssV2 = new CvssV2("testing", CvssV2.Type.PRIMARY, v2Data, severity, 0.0, 0.0, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE);
        return cvssV2;
    }
    
    /**
     * Sorts the offered list of vulnerabilities four times starting with the vulnerabilities arranged in different sequences
     * before sorting to check that sorting is stable for permutations of input sequence.<br> The input array is added to a
     * sortedSet in four different sequences: all elements in delivered order, all elements in reverse of the deliverd order, all
     * odd elements in sequence followed by even elements in sequence, all even elements in sequence followed by the odd
     * elements.<br> It is then validated that regardless of the sequence of adding vulnerabilties the final sort is the same.
     *
     * @param vulnerabilities
     *         A list of vulnerabilities to check for stable sorting behaviour
     */
    private void testSortStabilityForPermutations(final List<Vulnerability> vulnerabilities) {
        int vulCount = vulnerabilities.size();
        boolean oddHasOneMore = (vulCount % 2) != 0;

        int oddEvenEvenStartIdx = oddHasOneMore ? (vulCount / 2) + 1 : vulCount / 2;
        int evenOddOddStartIdx = vulCount / 2;

        int[] indexSequential = new int[vulCount];
        int[] indexOddEven = new int[vulCount];
        int[] indexEvenOdd = new int[vulCount];
        int[] indexReversed = new int[vulCount];
        for (int i = 0; i < vulnerabilities.size(); i++) {
            indexSequential[i] = i;
            indexReversed[vulCount - i - 1] = i;
            if (i % 2 == 0) {
                indexOddEven[i / 2] = i;
                indexEvenOdd[(i / 2) + evenOddOddStartIdx] = i;
            } else {
                indexOddEven[oddEvenEvenStartIdx + (i / 2)] = i;
                indexEvenOdd[i / 2] = i;
            }
        }

        List<int[]> permutations = new ArrayList<>();
        permutations.add(indexReversed);
        permutations.add(indexOddEven);
        permutations.add(indexEvenOdd);
        permutations.add(indexSequential);

        TreeSet<Vulnerability> prev = new TreeSet<>();
        TreeSet<Vulnerability> current = new TreeSet<>();
        int[] prevPermutation = null;
        for (int[] permutation : permutations) {
            if (prev.isEmpty()) {
                prevPermutation = permutation;
                for (Integer ix : permutation) {
                    prev.add(vulnerabilities.get(ix));
                }
            }
            for (Integer ix : permutation) {
                current.add(vulnerabilities.get(ix));
            }
            assertPermutationSortedEqual(prev.toArray(new Vulnerability[0]), current.toArray(new Vulnerability[0]), permutation,
                                         prevPermutation);
        }
    }

    private void assertPermutationSortedEqual(final Vulnerability[] prev, final Vulnerability[] current, final int[] permutation,
                                              final int[] prevPermutation) {
        assertArrayEquals(String.format("Differently sorted for permutation '%s' versus '%s'", Arrays.toString(prevPermutation),
                                        Arrays.toString(permutation)), prev, current);
    }

}
