import "@aws-cdk/assert/jest";
import { SynthUtils } from "@aws-cdk/assert";
import { Stack } from "aws-cdk-lib";
import path from "path";
import { NextJSLambdaEdge } from "../src";
import { Runtime, Function, Code } from "aws-cdk-lib/aws-lambda";
import { Certificate } from "aws-cdk-lib/aws-certificatemanager";
import { HostedZone } from "aws-cdk-lib/aws-route53";
import { LambdaEdgeEventType, CachePolicy } from "aws-cdk-lib/aws-cloudfront";

describe("CDK Construct", () => {
  it("passes correct lambda options to underlying lambdas when single value passed", () => {
    const stack = new Stack();
    new NextJSLambdaEdge(stack, "Stack", {
      serverlessBuildOutDir: path.join(__dirname, "fixtures/app"),
      runtime: Runtime.NODEJS_10_X,
      name: {
        defaultLambda: "NextDefaultLambda",
        apiLambda: "NextApiLambda",
        imageLambda: "NextImageLambda"
      }
    });

    const synthesizedStack = SynthUtils.toCloudFormation(stack);
    expect(synthesizedStack).toHaveResourceLike("AWS::Lambda::Function", {
      FunctionName: "NextDefaultLambda",
      Runtime: Runtime.NODEJS_10_X.name
    });
    expect(synthesizedStack).toHaveResourceLike("AWS::Lambda::Function", {
      FunctionName: "NextApiLambda",
      Runtime: Runtime.NODEJS_10_X.name
    });
    expect(synthesizedStack).toHaveResourceLike("AWS::Lambda::Function", {
      FunctionName: "NextImageLambda",
      Runtime: Runtime.NODEJS_10_X.name
    });
  });

  it("passes correct lambda options to underlying lambdas when object passed", () => {
    const stack = new Stack();
    new NextJSLambdaEdge(stack, "Stack", {
      serverlessBuildOutDir: path.join(__dirname, "fixtures/app"),
      name: {
        defaultLambda: "NextDefaultLambda",
        apiLambda: "NextApiLambda",
        imageLambda: "NextImageLambda"
      },
      runtime: {
        defaultLambda: Runtime.PYTHON_3_8,
        apiLambda: Runtime.GO_1_X,
        imageLambda: Runtime.JAVA_8_CORRETTO
      }
    });

    const synthesizedStack = SynthUtils.toCloudFormation(stack);
    expect(synthesizedStack).toHaveResourceLike("AWS::Lambda::Function", {
      FunctionName: "NextDefaultLambda",
      Runtime: Runtime.PYTHON_3_8.name
    });
    expect(synthesizedStack).toHaveResourceLike("AWS::Lambda::Function", {
      FunctionName: "NextApiLambda",
      Runtime: Runtime.GO_1_X.name
    });
    expect(synthesizedStack).toHaveResourceLike("AWS::Lambda::Function", {
      FunctionName: "NextImageLambda",
      Runtime: Runtime.JAVA_8_CORRETTO.name
    });
  });

  it("lambda cache policy passes correct headers to origin when specified", () => {
    const stack = new Stack();
    new NextJSLambdaEdge(stack, "Stack", {
      serverlessBuildOutDir: path.join(__dirname, "fixtures/app"),
      whiteListedHeaders: ["my-header"],
      cachePolicyName: {
        lambdaCache: "NextLambdaCache"
      }
    });

    const synthesizedStack = SynthUtils.toCloudFormation(stack);
    expect(synthesizedStack).toHaveResourceLike(
      "AWS::CloudFront::CachePolicy",
      {
        CachePolicyConfig: {
          Name: "NextLambdaCache",
          ParametersInCacheKeyAndForwardedToOrigin: {
            HeadersConfig: {
              HeaderBehavior: "whitelist",
              Headers: ["my-header"]
            }
          }
        }
      }
    );
  });

  it("lambda cache policy passes correct cookies to origin when specified", () => {
    const stack = new Stack();
    new NextJSLambdaEdge(stack, "Stack", {
      serverlessBuildOutDir: path.join(__dirname, "fixtures/app"),
      whiteListedCookies: ["my-cookie"],
      cachePolicyName: {
        lambdaCache: "NextLambdaCache"
      }
    });

    const synthesizedStack = SynthUtils.toCloudFormation(stack);
    expect(synthesizedStack).toHaveResourceLike(
      "AWS::CloudFront::CachePolicy",
      {
        CachePolicyConfig: {
          Name: "NextLambdaCache",
          ParametersInCacheKeyAndForwardedToOrigin: {
            CookiesConfig: {
              CookieBehavior: "whitelist",
              Cookies: ["my-cookie"]
            }
          }
        }
      }
    );
  });

  it("lambda cache policy passes all cookies to origin when not specified", () => {
    const stack = new Stack();
    new NextJSLambdaEdge(stack, "Stack", {
      serverlessBuildOutDir: path.join(__dirname, "fixtures/app"),
      cachePolicyName: {
        lambdaCache: "NextLambdaCache"
      }
    });

    const synthesizedStack = SynthUtils.toCloudFormation(stack);
    expect(synthesizedStack).toHaveResourceLike(
      "AWS::CloudFront::CachePolicy",
      {
        CachePolicyConfig: {
          Name: "NextLambdaCache",
          ParametersInCacheKeyAndForwardedToOrigin: {
            CookiesConfig: {
              CookieBehavior: "all"
            }
          }
        }
      }
    );
  });

  it("statics cache policy uses passed in policy if provided", () => {
    const stack = new Stack();
    new NextJSLambdaEdge(stack, "Stack", {
      serverlessBuildOutDir: path.join(__dirname, "fixtures/app"),
      nextStaticsCachePolicy: new CachePolicy(stack, "NextStaticsCache", {
        cachePolicyName: "customNextStaticsCache"
      })
    });

    const synthesizedStack = SynthUtils.toCloudFormation(stack);
    expect(synthesizedStack).toHaveResourceLike(
      "AWS::CloudFront::CachePolicy",
      {
        CachePolicyConfig: {
          Name: "customNextStaticsCache"
        }
      }
    );
  });

  it("image cache policy uses passed in policy if provided", () => {
    const stack = new Stack();
    new NextJSLambdaEdge(stack, "Stack", {
      serverlessBuildOutDir: path.join(__dirname, "fixtures/app"),
      nextImageCachePolicy: new CachePolicy(stack, "NextImageCache", {
        cachePolicyName: "customNextImageCache"
      })
    });

    const synthesizedStack = SynthUtils.toCloudFormation(stack);
    expect(synthesizedStack).toHaveResourceLike(
      "AWS::CloudFront::CachePolicy",
      {
        CachePolicyConfig: {
          Name: "customNextImageCache"
        }
      }
    );
  });

  it("lambda cache policy uses passed in policy if provided", () => {
    const stack = new Stack();
    new NextJSLambdaEdge(stack, "Stack", {
      serverlessBuildOutDir: path.join(__dirname, "fixtures/app"),
      nextLambdaCachePolicy: new CachePolicy(stack, "NextLambdaCache", {
        cachePolicyName: "customNextLambdaCache"
      })
    });

    const synthesizedStack = SynthUtils.toCloudFormation(stack);
    expect(synthesizedStack).toHaveResourceLike(
      "AWS::CloudFront::CachePolicy",
      {
        CachePolicyConfig: {
          Name: "customNextLambdaCache"
        }
      }
    );
  });

  it("creates resources required for a custom domain when specified", () => {
    const stack = new Stack();
    const certificate = Certificate.fromCertificateArn(
      stack,
      "Cert",
      "arn:partition:service:us-east-1:1234578:abc"
    );
    const domainName = "domain.com";
    const hostedZone = HostedZone.fromHostedZoneAttributes(stack, "Zone", {
      hostedZoneId: "123",
      zoneName: domainName
    });
    new NextJSLambdaEdge(stack, "Stack", {
      serverlessBuildOutDir: path.join(__dirname, "fixtures/app"),
      domain: {
        certificate,
        domainNames: [domainName],
        hostedZone
      }
    });

    const synthesizedStack = SynthUtils.toCloudFormation(stack);
    expect(synthesizedStack).toHaveResourceLike(
      "AWS::CloudFront::Distribution",
      {
        DistributionConfig: {
          Aliases: ["domain.com"],
          ViewerCertificate: {
            AcmCertificateArn: "arn:partition:service:us-east-1:1234578:abc"
          }
        }
      }
    );

    expect(synthesizedStack).toHaveResourceLike("AWS::Route53::RecordSet", {
      Name: "domain.com.",
      Type: "A",
      AliasTarget: {
        DNSName: {
          "Fn::GetAtt": ["StackNextJSDistribution03A07E47", "DomainName"]
        },
        HostedZoneId: {
          "Fn::FindInMap": [
            "AWSCloudFrontPartitionHostedZoneIdMap",
            {
              Ref: "AWS::Partition"
            },
            "zoneId"
          ]
        }
      },
      HostedZoneId: "123"
    });
  });

  it("does not create Route53 records when no domain specified", () => {
    const stack = new Stack();
    new NextJSLambdaEdge(stack, "Stack", {
      serverlessBuildOutDir: path.join(__dirname, "fixtures/app")
    });

    const synthesizedStack = SynthUtils.toCloudFormation(stack);
    expect(synthesizedStack).toCountResources("AWS::Route53::RecordSet", 0);
  });

  it("does not create an SQS queue if the app has no ISR pages", () => {
    const stack = new Stack();
    new NextJSLambdaEdge(stack, "Stack", {
      serverlessBuildOutDir: path.join(__dirname, "fixtures/app")
    });

    const synthesizedStack = SynthUtils.toCloudFormation(stack);
    expect(synthesizedStack).toCountResources("AWS::SQS::Queue", 0);
  });

  it("does create an SQS queue if the app has ISR pages", () => {
    const stack = new Stack();
    new NextJSLambdaEdge(stack, "Stack", {
      serverlessBuildOutDir: path.join(__dirname, "fixtures/app-with-isr")
    });

    const synthesizedStack = SynthUtils.toCloudFormation(stack);
    expect(synthesizedStack).toCountResources("AWS::SQS::Queue", 1);
  });

  it("configure distribution, but not Route53 records, with custom domain outside AWS", () => {
    const stack = new Stack();
    const certificate = Certificate.fromCertificateArn(
      stack,
      "Cert",
      "arn:partition:service:us-east-1:1234578:abc"
    );
    const domainName = "domain.com";
    new NextJSLambdaEdge(stack, "Stack", {
      serverlessBuildOutDir: path.join(__dirname, "fixtures/app"),
      domain: {
        certificate,
        domainNames: [domainName]
      }
    });

    const synthesizedStack = SynthUtils.toCloudFormation(stack);

    expect(synthesizedStack).toHaveResourceLike(
      "AWS::CloudFront::Distribution",
      {
        DistributionConfig: {
          Aliases: ["domain.com"],
          ViewerCertificate: {
            AcmCertificateArn: "arn:partition:service:us-east-1:1234578:abc"
          }
        }
      }
    );

    expect(synthesizedStack).toCountResources("AWS::Route53::RecordSet", 0);
  });

  it("concatenates edgeLambdas passed to defaultBehavior", () => {
    const stack = new Stack();

    const viewerRequestFunction = new Function(
      stack,
      "ViewerRequestEdgeFunction",
      {
        code: Code.fromInline(`module.handler = () => {}`),
        handler: "index.handler",
        runtime: Runtime.NODEJS_10_X,
        functionName: "viewerRequest-test"
      }
    );

    new NextJSLambdaEdge(stack, "Stack", {
      serverlessBuildOutDir: path.join(__dirname, "fixtures/app"),
      runtime: Runtime.NODEJS_10_X,
      defaultBehavior: {
        edgeLambdas: [
          {
            functionVersion: viewerRequestFunction.currentVersion,
            eventType: LambdaEdgeEventType.VIEWER_REQUEST
          }
        ]
      }
    });

    const synthesizedStack = SynthUtils.toCloudFormation(stack);

    expect(synthesizedStack).toHaveResourceLike(
      "AWS::CloudFront::Distribution",
      {
        DistributionConfig: {
          DefaultCacheBehavior: {
            LambdaFunctionAssociations: [
              {
                IncludeBody: true,
                EventType: LambdaEdgeEventType.ORIGIN_REQUEST.toString()
              },
              {
                EventType: LambdaEdgeEventType.ORIGIN_RESPONSE.toString()
              },
              {
                EventType: LambdaEdgeEventType.VIEWER_REQUEST.toString()
              }
            ]
          }
        }
      }
    );
  });
});
