package com.securityc4po.api.pentest.finding

import com.fasterxml.jackson.databind.ObjectMapper
import com.github.tomakehurst.wiremock.common.Json
import com.securityc4po.api.BaseDocumentationIntTest
import com.securityc4po.api.configuration.NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR
import com.securityc4po.api.configuration.RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE
import com.securityc4po.api.configuration.SIC_INNER_SHOULD_BE_STATIC
import com.securityc4po.api.pentest.Pentest
import com.securityc4po.api.pentest.PentestCategory
import com.securityc4po.api.pentest.PentestEntity
import com.securityc4po.api.pentest.PentestStatus
import com.securityc4po.api.project.PentestState
import com.securityc4po.api.project.Project
import com.securityc4po.api.project.ProjectEntity
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.mongodb.core.MongoTemplate
import org.springframework.data.mongodb.core.query.Query
import org.springframework.restdocs.operation.preprocess.Preprocessors
import org.springframework.restdocs.payload.JsonFieldType
import org.springframework.restdocs.payload.PayloadDocumentation
import org.springframework.restdocs.request.RequestDocumentation
import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation
import reactor.core.publisher.Mono

@SuppressFBWarnings(
    SIC_INNER_SHOULD_BE_STATIC,
    NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR,
    RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE
)
class FindingControllerDocumentationTest: BaseDocumentationIntTest() {

    @Autowired
    lateinit var mongoTemplate: MongoTemplate
    var mapper = ObjectMapper()

    @BeforeEach
    fun init() {
        configureAdminToken()
        persistBasicTestScenario()
    }

    @AfterEach
    fun destroy() {
        cleanUp()
    }

    @Nested
    inner class GetFindings {
        @Test
        fun getFindingsByPentestId() {
            val pentestTwoId = "43fbc63c-f624-11ec-b939-0242ac120002"
            webTestClient.get()
                .uri("/pentests/{pentestId}/findings", pentestTwoId)
                .header("Authorization", "Bearer $tokenAdmin")
                .exchange()
                .expectStatus().isOk
                .expectHeader().doesNotExist("")
                .expectBody().json(Json.write(getFindingsResponse()))
                .consumeWith(
                    WebTestClientRestDocumentation.document(
                        "{methodName}",
                        Preprocessors.preprocessRequest(
                            Preprocessors.prettyPrint(),
                            Preprocessors.modifyUris().removePort(),
                            Preprocessors.removeHeaders("Host", "Content-Length")
                        ),
                        Preprocessors.preprocessResponse(
                            Preprocessors.prettyPrint()
                        ),
                        RequestDocumentation.relaxedPathParameters(
                            RequestDocumentation.parameterWithName("pentestId").description("The id of the pentest you want to get the findings for")
                        ),
                        PayloadDocumentation.relaxedResponseFields(
                            PayloadDocumentation.fieldWithPath("[].id").type(JsonFieldType.STRING)
                                .description("The id of the requested findings"),
                            PayloadDocumentation.fieldWithPath("[].severity").type(JsonFieldType.STRING)
                                .description("The severity of the finding"),
                            PayloadDocumentation.fieldWithPath("[].title").type(JsonFieldType.STRING)
                                .description("The title of the requested finding"),
                            PayloadDocumentation.fieldWithPath("[].description").type(JsonFieldType.STRING)
                                .description("The description number of the finding"),
                            PayloadDocumentation.fieldWithPath("[].impact").type(JsonFieldType.STRING)
                                .description("The impact of the finding"),
                            PayloadDocumentation.fieldWithPath("[].affectedUrls").type(JsonFieldType.ARRAY)
                                .description("List of affected Urls of the finding"),
                            PayloadDocumentation.fieldWithPath("[].reproduction").type(JsonFieldType.STRING)
                                .description("The reproduction steps of the finding"),
                            PayloadDocumentation.fieldWithPath("[].mitigation").type(JsonFieldType.STRING)
                                .description("The example mitigation for the finding")
                        )
                    )
                )
        }

        private val findingOne = Finding(
            id = "ab62d365-1b1d-4da1-89bc-5496616e220f",
            severity = Severity.LOW,
            title = "Found Bug",
            description = "OTG-INFO-002 Bug",
            impact = "Service",
            affectedUrls = emptyList(),
            reproduction = "Step 1: Hack",
            mitigation = "None"
        )

        private fun getFindingsResponse() = listOf(
            findingOne.toFindingResponseBody()
        )
    }

    @Nested
    inner class GetFinding {
        @Test
        fun getFindingById() {
            val findingId = "ab62d365-1b1d-4da1-89bc-5496616e220f"
            webTestClient.get()
                .uri("/pentests/{findingId}/finding", findingId)
                .header("Authorization", "Bearer $tokenAdmin")
                .exchange()
                .expectStatus().isOk
                .expectHeader().doesNotExist("")
                .expectBody().json(Json.write(findingOne.toFindingResponseBody()))
                .consumeWith(
                    WebTestClientRestDocumentation.document(
                        "{methodName}",
                        Preprocessors.preprocessRequest(
                            Preprocessors.prettyPrint(),
                            Preprocessors.modifyUris().removePort(),
                            Preprocessors.removeHeaders("Host", "Content-Length")
                        ),
                        Preprocessors.preprocessResponse(
                            Preprocessors.prettyPrint()
                        ),
                        RequestDocumentation.relaxedPathParameters(
                            RequestDocumentation.parameterWithName("findingId").description("The id of the finding you want to get")
                        ),
                        PayloadDocumentation.relaxedResponseFields(
                            PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING)
                                .description("The id of the requested finding"),
                            PayloadDocumentation.fieldWithPath("severity").type(JsonFieldType.STRING)
                                .description("The severity of the finding"),
                            PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING)
                                .description("The title of the requested finding"),
                            PayloadDocumentation.fieldWithPath("description").type(JsonFieldType.STRING)
                                .description("The description number of the finding"),
                            PayloadDocumentation.fieldWithPath("impact").type(JsonFieldType.STRING)
                                .description("The impact of the finding"),
                            PayloadDocumentation.fieldWithPath("affectedUrls").type(JsonFieldType.ARRAY)
                                .description("List of affected Urls of the finding"),
                            PayloadDocumentation.fieldWithPath("reproduction").type(JsonFieldType.STRING)
                                .description("The reproduction steps of the finding"),
                            PayloadDocumentation.fieldWithPath("mitigation").type(JsonFieldType.STRING)
                                .description("The example mitigation for the finding")
                        )
                    )
                )
        }

        private val findingOne = Finding(
            id = "ab62d365-1b1d-4da1-89bc-5496616e220f",
            severity = Severity.LOW,
            title = "Found Bug",
            description = "OTG-INFO-002 Bug",
            impact = "Service",
            affectedUrls = emptyList(),
            reproduction = "Step 1: Hack",
            mitigation = "None"
        )
    }

    @Nested
    inner class SaveFinding {
        @Test
        fun saveFindingByPentestId() {
            val pentestTwoId = "43fbc63c-f624-11ec-b939-0242ac120002"
            webTestClient.post()
                .uri("/pentests/{pentestId}/finding", pentestTwoId)
                .header("Authorization", "Bearer $tokenAdmin")
                .body(Mono.just(findingBody), FindingRequestBody::class.java)
                .exchange()
                .expectStatus().isAccepted
                .expectHeader().doesNotExist("")
                .expectBody().json(Json.write(findingBody))
                .consumeWith(
                    WebTestClientRestDocumentation.document(
                        "{methodName}",
                        Preprocessors.preprocessRequest(
                            Preprocessors.prettyPrint(),
                            Preprocessors.modifyUris().removePort(),
                            Preprocessors.removeHeaders("Host", "Content-Length")
                        ),
                        Preprocessors.preprocessResponse(
                            Preprocessors.prettyPrint()
                        ),
                        RequestDocumentation.relaxedPathParameters(
                            RequestDocumentation.parameterWithName("pentestId").description("The id of the pentest you want to save the finding for")
                        ),
                        PayloadDocumentation.relaxedResponseFields(
                            PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING)
                                .description("The id of the saved finding"),
                            PayloadDocumentation.fieldWithPath("severity").type(JsonFieldType.STRING)
                                .description("The severity of the finding"),
                            PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING)
                                .description("The title of the finding"),
                            PayloadDocumentation.fieldWithPath("description").type(JsonFieldType.STRING)
                                .description("The description number of the finding"),
                            PayloadDocumentation.fieldWithPath("impact").type(JsonFieldType.STRING)
                                .description("The impact of the finding"),
                            PayloadDocumentation.fieldWithPath("affectedUrls").type(JsonFieldType.ARRAY)
                                .description("List of affected Urls of the finding"),
                            PayloadDocumentation.fieldWithPath("reproduction").type(JsonFieldType.STRING)
                                .description("The reproduction steps of the finding"),
                            PayloadDocumentation.fieldWithPath("mitigation").type(JsonFieldType.STRING)
                                .description("The example mitigation for the finding")
                        )
                    )
                )
        }

        private val findingBody = FindingRequestBody(
            severity = "MEDIUM",
            title = "Found another Bug",
            description = "Another OTG-INFO-002 Bug",
            impact = "Service",
            affectedUrls = emptyList(),
            reproduction = "Step 1: Hack more",
            mitigation = "Another None"
        )
    }

    @Nested
    inner class UpdateFinding {
        @Test
        fun updateFindingById() {
            val findingId = "ab62d365-1b1d-4da1-89bc-5496616e220f"
            webTestClient.patch()
                .uri("/pentests/{findingId}/finding", findingId)
                .header("Authorization", "Bearer $tokenAdmin")
                .body(Mono.just(findingBody), FindingRequestBody::class.java)
                .exchange()
                .expectStatus().isAccepted
                .expectHeader().doesNotExist("")
                .expectBody().json(Json.write(findingBody))
                .consumeWith(
                    WebTestClientRestDocumentation.document(
                        "{methodName}",
                        Preprocessors.preprocessRequest(
                            Preprocessors.prettyPrint(),
                            Preprocessors.modifyUris().removePort(),
                            Preprocessors.removeHeaders("Host", "Content-Length")
                        ),
                        Preprocessors.preprocessResponse(
                            Preprocessors.prettyPrint()
                        ),
                        RequestDocumentation.relaxedPathParameters(
                            RequestDocumentation.parameterWithName("findingId").description("The id of the finding you want to update")
                        ),
                        PayloadDocumentation.relaxedResponseFields(
                            PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING)
                                .description("The id of the updated finding"),
                            PayloadDocumentation.fieldWithPath("severity").type(JsonFieldType.STRING)
                                .description("The severity of the finding"),
                            PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING)
                                .description("The title of the requested finding"),
                            PayloadDocumentation.fieldWithPath("description").type(JsonFieldType.STRING)
                                .description("The description number of the finding"),
                            PayloadDocumentation.fieldWithPath("impact").type(JsonFieldType.STRING)
                                .description("The impact of the finding"),
                            PayloadDocumentation.fieldWithPath("affectedUrls").type(JsonFieldType.ARRAY)
                                .description("List of affected Urls of the finding"),
                            PayloadDocumentation.fieldWithPath("reproduction").type(JsonFieldType.STRING)
                                .description("The reproduction steps of the finding"),
                            PayloadDocumentation.fieldWithPath("mitigation").type(JsonFieldType.STRING)
                                .description("The example mitigation for the finding")
                        )
                    )
                )
        }

        private val findingBody = FindingRequestBody(
            severity = "HIGH",
            title = "Updated Bug",
            description = "Updated OTG-INFO-002 Bug",
            impact = "Service",
            affectedUrls = emptyList(),
            reproduction = "Step 1: Hack",
            mitigation = "Still None"
        )
    }

    @Nested
    inner class DeleteFinding {
        @Test
        fun deleteFindingByPentestAndFindingId() {
            val pentestId = "43fbc63c-f624-11ec-b939-0242ac120002"
            val findingId = "ab62d365-1b1d-4da1-89bc-5496616e220f"
            webTestClient.delete()
                .uri("/pentests/{pentestId}/finding/{findingId}", pentestId, findingId)
                .header("Authorization", "Bearer $tokenAdmin")
                .exchange()
                .expectStatus().isOk
                .expectHeader().doesNotExist("")
                .expectBody()
                .consumeWith(
                    WebTestClientRestDocumentation.document(
                        "{methodName}",
                        Preprocessors.preprocessRequest(
                            Preprocessors.prettyPrint(),
                            Preprocessors.modifyUris().removePort(),
                            Preprocessors.removeHeaders("Host", "Content-Length")
                        ),
                        Preprocessors.preprocessResponse(
                            Preprocessors.prettyPrint()
                        ),
                        RequestDocumentation.relaxedPathParameters(
                            RequestDocumentation.parameterWithName("pentestId").description("The id of the pentest you want to remove the finding from"),
                            RequestDocumentation.parameterWithName("findingId").description("The id of the finding you want to delete")
                        ),
                        PayloadDocumentation.relaxedResponseFields(
                            PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING)
                                .description("The id of the finding you deleted")
                        )
                    )
                )
        }
    }

    private fun persistBasicTestScenario() {
        // setup test data
        // Project
        val projectOne = Project(
            id = "d2e126ba-f608-11ec-b939-0242ac120025",
            client = "E Corp",
            title = "Some Mock API (v1.0) Scanning",
            createdAt = "2021-01-10T18:05:00Z",
            tester = "Novatester",
            projectPentests = emptyList(),
            state = PentestState.NEW,
            version = "1.0",
            createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032"
        )
        // Pentests
        val pentestOne = Pentest(
            id = "9c8af320-f608-11ec-b939-0242ac120002",
            projectId = "d2e126ba-f608-11ec-b939-0242ac120025",
            category = PentestCategory.INFORMATION_GATHERING,
            refNumber = "OTG-INFO-001",
            status = PentestStatus.NOT_STARTED,
            enabled = true,
            findingIds = emptyList(),
            commentIds = emptyList(),
            timeSpent = 0
        )
        val pentestTwo = Pentest(
            id = "43fbc63c-f624-11ec-b939-0242ac120002",
            projectId = "d2e126ba-f608-11ec-b939-0242ac120025",
            category = PentestCategory.INFORMATION_GATHERING,
            refNumber = "OTG-INFO-002",
            status = PentestStatus.IN_PROGRESS,
            enabled = true,
            findingIds = listOf("ab62d365-1b1d-4da1-89bc-5496616e220f"),
            commentIds = emptyList(),
            timeSpent = 56
        )
        val pentestThree = Pentest(
            id = "74eae112-f62c-11ec-b939-0242ac120002",
            projectId = "d2e126ba-f608-11ec-b939-0242ac120025",
            category = PentestCategory.AUTHENTICATION_TESTING,
            refNumber = "OTG-AUTHN-001",
            status = PentestStatus.COMPLETED,
            enabled = true,
            findingIds = emptyList(),
            commentIds = emptyList(),
            timeSpent = 124
        )
        // Finding
        val findingOne = Finding(
            id = "ab62d365-1b1d-4da1-89bc-5496616e220f",
            severity = Severity.LOW,
            title = "Found Bug",
            description = "OTG-INFO-002 Bug",
            impact = "Service",
            affectedUrls = emptyList(),
            reproduction = "Step 1: Hack",
            mitigation = "None"
        )
        // persist test data in database
        mongoTemplate.save(ProjectEntity(projectOne))
        mongoTemplate.save(PentestEntity(pentestOne))
        mongoTemplate.save(PentestEntity(pentestTwo))
        mongoTemplate.save(PentestEntity(pentestThree))
        mongoTemplate.save(FindingEntity(findingOne))
    }

    private fun configureAdminToken() {
        tokenAdmin = getAccessToken("test_admin", "test", "c4po_local", "c4po_realm_local")
    }

    private fun cleanUp() {
        mongoTemplate.findAllAndRemove(Query(), ProjectEntity::class.java)
        mongoTemplate.findAllAndRemove(Query(), PentestEntity::class.java)
        mongoTemplate.findAllAndRemove(Query(), FindingEntity::class.java)

        tokenAdmin = "n/a"
    }
}