//  Copyright (c) 2020 650 Industries, Inc. All rights reserved.

import ExpoModulesTestCore

@testable import EXUpdates

class CodeSigningConfigurationSpec : ExpoSpec {
  override func spec() {
    it("works with spearate certificate chain") {
      let leafCert = getTestCertificate(TestCertificate.chainLeaf)
      let intermediateCert = getTestCertificate(TestCertificate.chainIntermediate)
      let rootCert = getTestCertificate(TestCertificate.chainRoot)

      let testCert = getTestCertificate(TestCertificate.test)

      let chain1 = CodeSigningConfiguration.separateCertificateChain(certificateChainInManifestResponse: leafCert)
      expect(chain1.count) == 1

      let chain2 = CodeSigningConfiguration.separateCertificateChain(certificateChainInManifestResponse: leafCert + intermediateCert)
      expect(chain2.count) == 2

      let chain3 = CodeSigningConfiguration.separateCertificateChain(certificateChainInManifestResponse: leafCert + intermediateCert + rootCert)
      expect(chain3.count) == 3

      let chainWithABunchOfNewlinesAndStuff  = CodeSigningConfiguration.separateCertificateChain(
        certificateChainInManifestResponse: testCert + "\n\n\n\n" + testCert
      )
      expect(chainWithABunchOfNewlinesAndStuff.count) == 2
    }

    describe("createAcceptSignatureHeader") {
      it("creates signature header default values") {
        let cert = getTestCertificate(TestCertificate.test)
        let configuration = try CodeSigningConfiguration(
          embeddedCertificateString: cert,
          metadata: [:],
          includeManifestResponseCertificateChain: false,
          allowUnsignedManifests: false
        )
        let signatureHeader = configuration.createAcceptSignatureHeader()
        expect(signatureHeader) == "sig, keyid=\"root\", alg=\"rsa-v1_5-sha256\""
      }

      it("creates signature header values from config") {
        let cert = getTestCertificate(TestCertificate.test)
        let configuration = try CodeSigningConfiguration(
          embeddedCertificateString: cert,
          metadata: [
            CodeSigningMetadataFields.AlgorithmFieldKey: CodeSigningAlgorithm.RSA_SHA256.rawValue,
            CodeSigningMetadataFields.KeyIdFieldKey: "test"
          ],
          includeManifestResponseCertificateChain: false,
          allowUnsignedManifests: false
        )
        let signatureHeader = configuration.createAcceptSignatureHeader()
        expect(signatureHeader) == "sig, keyid=\"test\", alg=\"rsa-v1_5-sha256\""
      }

      it("creates signature header escaped values") {
        let cert = getTestCertificate(TestCertificate.test)
        let configuration = try CodeSigningConfiguration(
          embeddedCertificateString: cert,
          metadata: [
            CodeSigningMetadataFields.AlgorithmFieldKey: CodeSigningAlgorithm.RSA_SHA256.rawValue,
            CodeSigningMetadataFields.KeyIdFieldKey: #"test"hello\"#
          ],
          includeManifestResponseCertificateChain: false,
          allowUnsignedManifests: false
        )
        let signatureHeader = configuration.createAcceptSignatureHeader()
        expect(signatureHeader) == #"sig, keyid="test\"hello\\", alg="rsa-v1_5-sha256""#
      }

      it("creates signature header throws invalid arg") {
        let cert = getTestCertificate(TestCertificate.test)
        expect {
          try CodeSigningConfiguration(
            embeddedCertificateString: cert,
            metadata: [
              CodeSigningMetadataFields.AlgorithmFieldKey: "fake",
              CodeSigningMetadataFields.KeyIdFieldKey: "test"
            ],
            includeManifestResponseCertificateChain: false,
            allowUnsignedManifests: false)
        }.to(throwError(CodeSigningError.AlgorithmParseError))
      }
    }

    describe("validateSignature") {
      it("works for valid case") {
        let cert = getTestCertificate(TestCertificate.test)
        let configuration = try CodeSigningConfiguration(
          embeddedCertificateString: cert,
          metadata: [:],
          includeManifestResponseCertificateChain: false,
          allowUnsignedManifests: false
        )
        let signatureValidationResult = try configuration.validateSignature(
          signature: CertificateFixtures.testNewManifestBodySignature,
          signedData: CertificateFixtures.testNewManifestBody.data(using: .utf8)!,
          manifestResponseCertificateChain: nil
        )
        expect(signatureValidationResult.validationResult) == ValidationResult.valid
        expect(signatureValidationResult.expoProjectInformation).to(beNil())
      }

      it("returns false when signature is invalid") {
        let cert = getTestCertificate(TestCertificate.test)
        let configuration = try CodeSigningConfiguration(
          embeddedCertificateString: cert,
          metadata: [:],
          includeManifestResponseCertificateChain: false,
          allowUnsignedManifests: false
        )
        let signatureValidationResult = try configuration.validateSignature(
          signature: "sig=\"aGVsbG8=\"",
          signedData: CertificateFixtures.testNewManifestBody.data(using: .utf8)!,
          manifestResponseCertificateChain: nil
        )
        expect(signatureValidationResult.validationResult) == ValidationResult.invalid
        expect(signatureValidationResult.expoProjectInformation).to(beNil())
      }

      it("throws when key does not match") {
        let cert = getTestCertificate(TestCertificate.test)
        let configuration = try CodeSigningConfiguration(
          embeddedCertificateString: cert,
          metadata: [CodeSigningMetadataFields.KeyIdFieldKey: "test"],
          includeManifestResponseCertificateChain: false,
          allowUnsignedManifests: false
        )
        expect {
          try configuration.validateSignature(
            signature: "sig=\"aGVsbG8=\", keyid=\"other\"",
            signedData: CertificateFixtures.testNewManifestBody.data(using: .utf8)!,
            manifestResponseCertificateChain: nil
          )
        }.to(throwError(CodeSigningError.KeyIdMismatchError))
      }

      it("does not use chain in manifest response if flag is false") {
        let testCert = getTestCertificate(TestCertificate.test)
        let leafCert = getTestCertificate(TestCertificate.chainLeaf)
        let intermediateCert = getTestCertificate(TestCertificate.chainIntermediate)
        let configuration = try CodeSigningConfiguration(
          embeddedCertificateString: testCert,
          metadata: [:],
          includeManifestResponseCertificateChain: false,
          allowUnsignedManifests: false
        )
        let signatureValidationResult = try configuration.validateSignature(
          signature: CertificateFixtures.testNewManifestBodySignature,
          signedData: CertificateFixtures.testNewManifestBody.data(using: .utf8)!,
          manifestResponseCertificateChain: leafCert + intermediateCert
        )
        expect(signatureValidationResult.validationResult) == ValidationResult.valid
        expect(signatureValidationResult.expoProjectInformation).to(beNil())
      }

      it("does use chain in manifest response if flag is true") {
        let leafCert = getTestCertificate(TestCertificate.chainLeaf)
        let intermediateCert = getTestCertificate(TestCertificate.chainIntermediate)
        let rootCert = getTestCertificate(TestCertificate.chainRoot)
        let configuration = try CodeSigningConfiguration(
          embeddedCertificateString: rootCert,
          metadata: [CodeSigningMetadataFields.KeyIdFieldKey: "ca-root"],
          includeManifestResponseCertificateChain: true,
          allowUnsignedManifests: false
        )
        let signatureValidationResult = try configuration.validateSignature(
          signature: CertificateFixtures.testNewManifestBodyValidChainLeafSignature,
          signedData: CertificateFixtures.testNewManifestBody.data(using: .utf8)!,
          manifestResponseCertificateChain: leafCert + intermediateCert
        )
        expect(signatureValidationResult.validationResult) == ValidationResult.valid

        let expoProjectInformation = signatureValidationResult.expoProjectInformation
        expect(expoProjectInformation?.scopeKey) == "@test/app"
        expect(expoProjectInformation?.projectId) == "285dc9ca-a25d-4f60-93be-36dc312266d7"
      }

      it("AllowsUnsignedManifestIfAllowUnsignedFlagIsTrue") {
        let testCert = getTestCertificate(TestCertificate.test)
        let configuration = try CodeSigningConfiguration(
          embeddedCertificateString: testCert,
          metadata: [CodeSigningMetadataFields.KeyIdFieldKey: "test"],
          includeManifestResponseCertificateChain: true,
          allowUnsignedManifests: true
        )
        let signatureValidationResult = try configuration.validateSignature(
          signature: nil,
          signedData: CertificateFixtures.testNewManifestBody.data(using: .utf8)!,
          manifestResponseCertificateChain: nil
        )
        expect(signatureValidationResult.validationResult) == ValidationResult.skipped
        expect(signatureValidationResult.expoProjectInformation).to(beNil())
      }

      it("ChecksSignedManifestIfAllowUnsignedFlagIsTrueButSignatureIsProvided") {
        let testCert = getTestCertificate(TestCertificate.test)
        let configuration = try CodeSigningConfiguration(
          embeddedCertificateString: testCert,
          metadata: [CodeSigningMetadataFields.KeyIdFieldKey: "test"],
          includeManifestResponseCertificateChain: true,
          allowUnsignedManifests: true
        )
        let signatureValidationResult = try configuration.validateSignature(
          signature: "sig=\"aGVsbG8=\"",
          signedData: CertificateFixtures.testNewManifestBody.data(using: .utf8)!,
          manifestResponseCertificateChain: nil
        )
        expect(signatureValidationResult.validationResult) == ValidationResult.invalid
        expect(signatureValidationResult.expoProjectInformation).to(beNil())
      }
    }
  }
}
