/*
 * This file is part of Dependency-Track.
 *
 * 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.
 *
 * SPDX-License-Identifier: Apache-2.0
 * Copyright (c) OWASP Foundation. All Rights Reserved.
 */
package org.dependencytrack.tasks;

import alpine.event.framework.Event;
import alpine.event.framework.EventService;
import org.dependencytrack.PersistenceCapableTest;
import org.dependencytrack.event.ComponentMetricsUpdateEvent;
import org.dependencytrack.event.ComponentVulnerabilityAnalysisEvent;
import org.dependencytrack.event.GitHubAdvisoryMirrorEvent;
import org.dependencytrack.event.PortfolioVulnerabilityAnalysisEvent;
import org.dependencytrack.event.ProjectMetricsUpdateEvent;
import org.dependencytrack.event.ProjectVulnerabilityAnalysisEvent;
import org.dependencytrack.model.Component;
import org.dependencytrack.model.Project;
import org.dependencytrack.model.Vulnerability;
import org.dependencytrack.model.VulnerabilityAnalysisLevel;
import org.dependencytrack.model.VulnerableSoftware;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.time.Duration;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.awaitility.Awaitility.await;
import static org.dependencytrack.model.ConfigPropertyConstants.SCANNER_INTERNAL_ENABLED;

class VulnerabilityAnalysisTaskTest extends PersistenceCapableTest {

    private static final ConcurrentLinkedQueue<Event> EVENTS = new ConcurrentLinkedQueue<>();

    public static class EventSubscriber implements alpine.event.framework.Subscriber {

        @Override
        public void inform(final Event event) {
            EVENTS.add(event);
        }

    }

    @BeforeEach
    public void before() throws Exception {
        EventService.getInstance().subscribe(ComponentMetricsUpdateEvent.class, EventSubscriber.class);
        EventService.getInstance().subscribe(ProjectMetricsUpdateEvent.class, EventSubscriber.class);
    }

    @AfterEach
    public void after() {
        EventService.getInstance().unsubscribe(EventSubscriber.class);
        EVENTS.clear();
    }

    @Test
    void shouldAnalyzeComponent() {
        qm.createConfigProperty(
                SCANNER_INTERNAL_ENABLED.getGroupName(),
                SCANNER_INTERNAL_ENABLED.getPropertyName(),
                "true",
                SCANNER_INTERNAL_ENABLED.getPropertyType(),
                SCANNER_INTERNAL_ENABLED.getDescription());

        final var project = new Project();
        project.setName("acme-app");
        project.setVersion("1.0.0");
        qm.persist(project);

        final var component = new Component();
        component.setProject(project);
        component.setName("acme-lib");
        component.setVersion("2.0.0");
        component.setPurl("pkg:maven/com.acme/acme-lib@2.0.0");
        qm.persist(component);

        final var vuln = new Vulnerability();
        vuln.setVulnId("INT-123");
        vuln.setSource(Vulnerability.Source.INTERNAL);
        qm.persist(vuln);

        final var vs = new VulnerableSoftware();
        vs.setPurlType("maven");
        vs.setPurlNamespace("com.acme");
        vs.setPurlName("acme-lib");
        vs.setVersion("2.0.0");
        vs.setVulnerabilities(List.of(vuln));
        qm.persist(vs);

        new VulnerabilityAnalysisTask().inform(
                new ComponentVulnerabilityAnalysisEvent(component));

        assertThat(qm.getAllVulnerabilities(component)).hasSize(1);
    }

    @Test
    void shouldAnalyzeProject() {
        qm.createConfigProperty(
                SCANNER_INTERNAL_ENABLED.getGroupName(),
                SCANNER_INTERNAL_ENABLED.getPropertyName(),
                "true",
                SCANNER_INTERNAL_ENABLED.getPropertyType(),
                SCANNER_INTERNAL_ENABLED.getDescription());

        final var project = new Project();
        project.setName("acme-app");
        project.setVersion("1.0.0");
        qm.persist(project);

        final var component = new Component();
        component.setProject(project);
        component.setName("acme-lib");
        component.setVersion("2.0.0");
        component.setPurl("pkg:maven/com.acme/acme-lib@2.0.0");
        qm.persist(component);

        final var vuln = new Vulnerability();
        vuln.setVulnId("INT-123");
        vuln.setSource(Vulnerability.Source.INTERNAL);
        qm.persist(vuln);

        final var vs = new VulnerableSoftware();
        vs.setPurlType("maven");
        vs.setPurlNamespace("com.acme");
        vs.setPurlName("acme-lib");
        vs.setVersion("2.0.0");
        vs.setVulnerabilities(List.of(vuln));
        qm.persist(vs);

        new VulnerabilityAnalysisTask().inform(
                new ProjectVulnerabilityAnalysisEvent(project, VulnerabilityAnalysisLevel.ON_DEMAND));

        assertThat(qm.getAllVulnerabilities(component)).hasSize(1);

        // For analysis of individual projects, metrics updates are expected to
        // be initiated via event chaining.
        assertThat(EVENTS).isEmpty();

        qm.getPersistenceManager().evictAll();
        assertThat(project.getLastVulnerabilityAnalysis()).isNotNull();
    }

    @Test
    void shouldAnalyzePortfolio() {
        qm.createConfigProperty(
                SCANNER_INTERNAL_ENABLED.getGroupName(),
                SCANNER_INTERNAL_ENABLED.getPropertyName(),
                "true",
                SCANNER_INTERNAL_ENABLED.getPropertyType(),
                SCANNER_INTERNAL_ENABLED.getDescription());

        final var activeProject = new Project();
        activeProject.setName("acme-app");
        activeProject.setVersion("1.0.0");
        qm.persist(activeProject);

        final var activeComponent = new Component();
        activeComponent.setProject(activeProject);
        activeComponent.setName("acme-lib");
        activeComponent.setVersion("2.0.0");
        activeComponent.setPurl("pkg:maven/com.acme/acme-lib@2.0.0");
        qm.persist(activeComponent);

        final var inactiveProject = new Project();
        inactiveProject.setName("acme-app-b");
        inactiveProject.setVersion("1.0.0");
        inactiveProject.setActive(false);
        qm.persist(inactiveProject);

        final var inactiveComponent = new Component();
        inactiveComponent.setProject(inactiveProject);
        inactiveComponent.setName("acme-lib");
        inactiveComponent.setVersion("2.0.0");
        inactiveComponent.setPurl("pkg:maven/com.acme/acme-lib@2.0.0");
        qm.persist(inactiveComponent);

        final var vuln = new Vulnerability();
        vuln.setVulnId("INT-123");
        vuln.setSource(Vulnerability.Source.INTERNAL);
        qm.persist(vuln);

        final var vs = new VulnerableSoftware();
        vs.setPurlType("maven");
        vs.setPurlNamespace("com.acme");
        vs.setPurlName("acme-lib");
        vs.setVersion("2.0.0");
        vs.setVulnerabilities(List.of(vuln));
        qm.persist(vs);

        new VulnerabilityAnalysisTask().inform(
                new PortfolioVulnerabilityAnalysisEvent());

        assertThat(qm.getAllVulnerabilities(activeComponent)).hasSize(1);
        assertThat(qm.getAllVulnerabilities(inactiveComponent)).isEmpty();

        await("Event reception")
                .atMost(Duration.ofSeconds(3))
                .untilAsserted(() -> assertThat(EVENTS).hasSize(1));

        assertThat(EVENTS).satisfiesExactly(event -> {
            assertThat(event).isInstanceOf(ProjectMetricsUpdateEvent.class);

            final var metricsUpdateEvent = (ProjectMetricsUpdateEvent) event;
            assertThat(metricsUpdateEvent.getUuid()).isEqualTo(activeProject.getUuid());
        });
    }

    @Test
    void shouldThrowWhenInformedAboutUnexpectedEvent() {
        assertThatExceptionOfType(IllegalArgumentException.class)
                .isThrownBy(() -> new VulnerabilityAnalysisTask().inform(new GitHubAdvisoryMirrorEvent()));
    }

    @Test
    void shouldNotThrowWhenProjectDoesNotExist() {
        final var dummyProject = new Project();
        dummyProject.setUuid(UUID.randomUUID());

        final var event = new ProjectVulnerabilityAnalysisEvent(dummyProject, VulnerabilityAnalysisLevel.ON_DEMAND);

        assertThatNoException()
                .isThrownBy(() -> new VulnerabilityAnalysisTask().inform(event));
    }

}
