// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0

package order

import (
	"bytes"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"sigs.k8s.io/kustomize/kyaml/kio"
	"sigs.k8s.io/kustomize/kyaml/yaml"
)

func TestSyncOrder(t *testing.T) {
	testCases := []struct {
		name     string
		from     string
		to       string
		expected string
	}{
		{
			name: "sort data fields configmap with comments",
			from: `apiVersion: v1
kind: ConfigMap
metadata:
  name: setters-config
data:
  # This should be the name of your Config Controller instance
  cluster-name: cluster-name
  # This should be the project where you deployed Config Controller
  project-id: project-id # pro
  project-number: "1234567890123"
  # You can leave these defaults
  namespace: config-control
  deployment-repo: deployment-repo
  source-repo: source-repo
`,
			to: `apiVersion: v1
kind: ConfigMap
metadata: # kpt-merge: /setters-config
  name: setters-config
data:
  # You can leave these defaults
  namespace: config-control
  # This should be the name of your Config Controller instance
  cluster-name: cluster-name
  deployment-repo: deployment-repo
  # This should be the project where you deployed Config Controller
  project-id: project-id # project
  project-number: "1234567890123"
  source-repo: source-repo
`,
			expected: `apiVersion: v1
kind: ConfigMap
metadata: # kpt-merge: /setters-config
  name: setters-config
data:
  # This should be the name of your Config Controller instance
  cluster-name: cluster-name
  # This should be the project where you deployed Config Controller
  project-id: project-id # project
  project-number: "1234567890123"
  # You can leave these defaults
  namespace: config-control
  deployment-repo: deployment-repo
  source-repo: source-repo
`,
		},
		{
			name: "sort data fields configmap but retain order of extra fields",
			from: `apiVersion: v1
kind: ConfigMap
data:
  baz: bar
  cluster-name: cluster-name
  foo: config-control
`,
			to: `kind: ConfigMap
apiVersion: v1
metadata:
  name: foo
data:
  color: orange
  foo: config-control
  abc: def
  cluster-name: cluster-name
`,
			expected: `apiVersion: v1
kind: ConfigMap
data:
  cluster-name: cluster-name
  foo: config-control
  color: orange
  abc: def
metadata:
  name: foo
`,
		},
		{
			name: "sort containers list node with sequence head comments preservation",
			from: `apiVersion: apps/v1
kind: Deployment
metadata:
  name: before
spec:
  containers:
  - name: nginx
    # nginx image
    image: "nginx:1.16.1"
    ports:
    - protocol: TCP # tcp protocol
      containerPort: 80
`,
			to: `apiVersion: apps/v1
kind: Deployment
metadata:
  name: after
spec:
  containers:
  # ports comment
  - ports:
    - containerPort: 80
      protocol: TCP # tcp protocol
    # nginx image
    image: "nginx:1.16.2"
    # nginx container
    name: nginx
# end of resource
`,
			expected: `apiVersion: apps/v1
kind: Deployment
metadata:
  name: after
spec:
  containers:
  # ports comment
  # nginx container
  - name: nginx
    # nginx image
    image: "nginx:1.16.2"
    ports:
    - protocol: TCP # tcp protocol
      containerPort: 80
# end of resource
`,
		},
		{
			name: "Do not alter sequence order",
			from: `apiVersion: v1
kind: KRMFile
metadata:
  name: before
pipeline:
  mutators:
  - image: apply-setters:v0.1
    configPath: setters.yaml
  - image: set-namespace:v0.1
    configPath: ns.yaml
`,
			to: `apiVersion: v1
kind: KRMFile
metadata:
  name: after
pipeline:
  mutators:
  - configPath: sr.yaml
    image: search-replace:v0.1
  - image: apply-setters:v0.1
    configPath: setters.yaml
  - image: set-namespace:v0.1
    configPath: ns.yaml
`,
			expected: `apiVersion: v1
kind: KRMFile
metadata:
  name: after
pipeline:
  mutators:
  - image: search-replace:v0.1
    configPath: sr.yaml
  - image: apply-setters:v0.1
    configPath: setters.yaml
  - image: set-namespace:v0.1
    configPath: ns.yaml
`,
		},
		{
			name: "sort fields with null value",
			from: `apiVersion: v1
kind: ConfigMap
metadata:
  creationTimestamp: null
  name: workspaces.app.terraform.io
`,
			to: `apiVersion: v1
kind: ConfigMap
metadata:
  name: workspaces.app.terraform.io
  creationTimestamp: null
`,
			expected: `apiVersion: v1
kind: ConfigMap
metadata:
  creationTimestamp: null
  name: workspaces.app.terraform.io
`,
		},
		{
			name: "Complex ASM reorder example",
			from: `apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: (unknown)
  creationTimestamp: null
  name: controlplanerevisions.mesh.cloud.google.com
spec:
  group: mesh.cloud.google.com
  names:
    kind: ControlPlaneRevision
    listKind: ControlPlaneRevisionList
    plural: controlplanerevisions
    singular: controlplanerevision
  scope: Namespaced
  subresources:
    status: {}
  validation:
    openAPIV3Schema:
      description: ControlPlaneRevision is the Schema for the ControlPlaneRevision API
      properties:
        apiVersion:
          description: 'APIVersion'
          type: string
        kind:
          description: 'Kind'
          type: string
        metadata:
          type: object
        spec:
          description: ControlPlaneRevisionSpec defines the desired state of ControlPlaneRevision
          properties:
            channel:
              description: ReleaseChannel determines the aggressiveness of upgrades.
              enum:
              - regular
              - rapid
              - stable
              type: string
            type:
              description: ControlPlaneRevisionType determines how the revision should be managed.
              enum:
              - managed_service
              type: string
          type: object
        status:
          description: ControlPlaneRevisionStatus defines the observed state of ControlPlaneRevision.
          properties:
            conditions:
              items:
                description: ControlPlaneRevisionCondition is a repeated struct defining the current conditions of a ControlPlaneRevision.
                properties:
                  lastTransitionTime:
                    description: Last time the condition transitioned from one status to another
                    format: date-time
                    type: string
                  message:
                    description: Human-readable message indicating details about last transition
                    type: string
                  reason:
                    description: Unique, one-word, CamelCase reason for the condition's last transition
                    type: string
                  status:
                    description: Status is the status of the condition. Can be True, False, or Unknown.
                    type: string
                  type:
                    description: Type is the type of the condition.
                    type: string
                type: object
              type: array
          type: object
      type: object
  version: v1alpha1
  versions:
  - name: v1alpha1
    served: true
    storage: true
status:
  acceptedNames:
    kind: ""
    plural: ""
  conditions: []
  storedVersions: []
`,
			to: `apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: controlplanerevisions.mesh.cloud.google.com
  annotations:
    controller-gen.kubebuilder.io/version: (unknown)
  creationTimestamp: null
spec:
  group: mesh.cloud.google.com
  names:
    kind: ControlPlaneRevision
    listKind: ControlPlaneRevisionList
    plural: controlplanerevisions
    singular: controlplanerevision
  scope: Namespaced
  subresources:
    status: {}
  validation:
    openAPIV3Schema:
      type: object
      description: ControlPlaneRevision is the Schema for the ControlPlaneRevision API
      properties:
        apiVersion:
          type: string
          description: 'APIVersion'
        kind:
          type: string
          description: 'Kind'
        metadata:
          type: object
        spec:
          type: object
          description: ControlPlaneRevisionSpec defines the desired state of ControlPlaneRevision
          properties:
            type:
              type: string
              description: ControlPlaneRevisionType determines how the revision should be managed.
              enum:
                - managed_service
            channel:
              type: string
              description: ReleaseChannel determines the aggressiveness of upgrades.
              enum:
                - regular
                - rapid
                - stable
        status:
          type: object
          description: ControlPlaneRevisionStatus defines the observed state of ControlPlaneRevision.
          properties:
            conditions:
              type: array
              items:
                type: object
                description: ControlPlaneRevisionCondition is a repeated struct defining the current conditions of a ControlPlaneRevision.
                properties:
                  type:
                    type: string
                    description: Type is the type of the condition.
                  status:
                    type: string
                    description: Status is the status of the condition. Can be True, False, or Unknown.
                  lastTransitionTime:
                    type: string
                    description: Last time the condition transitioned from one status to another
                    format: date-time
                  message:
                    type: string
                    description: Human-readable message indicating details about last transition
                  reason:
                    type: string
                    description: Unique, one-word, CamelCase reason for the condition's last transition
  version: v1alpha1
  versions:
    - name: v1alpha1
      served: true
      storage: true
status:
  acceptedNames:
    kind: ""
    plural: ""
  conditions: []
  storedVersions: []
`,
			expected: `test.from`,
		},
	}

	for i := range testCases {
		tc := testCases[i]
		t.Run(tc.name, func(t *testing.T) {
			from, err := yaml.Parse(tc.from)
			if !assert.NoError(t, err) {
				t.FailNow()
			}

			to, err := yaml.Parse(tc.to)
			if !assert.NoError(t, err) {
				t.FailNow()
			}

			err = SyncOrder(from, to)
			if !assert.NoError(t, err) {
				t.FailNow()
			}

			out := &bytes.Buffer{}
			kio.ByteWriter{
				Writer:                out,
				KeepReaderAnnotations: false,
			}.Write([]*yaml.RNode{to})

			// this means "to" is just a reordered version of "from" and after syncing order,
			// resultant "to" must be equal to "from"
			if tc.expected == "test.from" {
				tc.expected = tc.from
			}

			if !assert.Equal(t, tc.expected, out.String()) {
				t.FailNow()
			}

			actualFrom, err := from.String()
			if !assert.NoError(t, err) {
				t.FailNow()
			}

			if !assert.Equal(t, strings.TrimSpace(tc.from), strings.TrimSpace(actualFrom)) {
				t.FailNow()
			}
		})
	}
}
