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

package krusty_test

import (
	"os"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	kusttest_test "sigs.k8s.io/kustomize/api/testutils/kusttest"
	"sigs.k8s.io/kustomize/kyaml/openapi"
	"sigs.k8s.io/kustomize/kyaml/openapi/kubernetesapi"
)

func writeTestSchema(th kusttest_test.Harness, filepath string) {
	bytes, _ := os.ReadFile("./testdata/customschema.json")
	th.WriteF(filepath+"mycrd_schema.json", string(bytes))
}

func writeTestSchemaYaml(th kusttest_test.Harness, filepath string) {
	bytes, _ := os.ReadFile("./testdata/customschema.yaml")
	th.WriteF(filepath+"mycrd_schema.yaml", string(bytes))
}

func writeCustomResource(th kusttest_test.Harness, filepath string) {
	th.WriteF(filepath, `
apiVersion: example.com/v1alpha1
kind: MyCRD
metadata:
  name: service
spec:
  template:
    spec:
      containers:
      - name: server
        image: server
        command: example
        ports:
        - name: grpc
          protocol: TCP
          containerPort: 8080
`)
}

func writeOtherCustomResource(th kusttest_test.Harness, filepath string) {
	th.WriteF(filepath, `
apiVersion: v1alpha1
kind: MyCRD
metadata:
  name: service
spec:
  template:
    spec:
      containers:
      - name: server
        image: server
        command: example
        ports:
        - name: grpc
          protocol: TCP
          containerPort: 8080
`)
}

func writeTestComponentWithCustomSchema(th kusttest_test.Harness) {
	writeTestSchema(th, "comp/")
	openapi.ResetOpenAPI()
	th.WriteC("comp", `
openapi:
  path: mycrd_schema.json
`)
	th.WriteF("comp/stub.yaml", `
apiVersion: v1
kind: Deployment
metadata:
  name: stub
spec:
  replicas: 1
`)
}

const customSchemaPatch = `
patchesStrategicMerge:
- |-
  apiVersion: example.com/v1alpha1
  kind: MyCRD
  metadata:
    name: service
  spec:
    template:
      spec:
        containers:
        - name: server
          image: nginx
`

const customSchemaPatchMultipleGvks = `
patchesStrategicMerge:
- |-
  apiVersion: example.com/v1alpha1
  kind: MyCRD
  metadata:
    name: service
  spec:
    template:
      spec:
        containers:
        - name: server
          image: nginx
- |-
  apiVersion: v1alpha1
  kind: MyCRD
  metadata:
    name: service
  spec:
    template:
      spec:
        containers:
        - name: server
          image: nginx
`

const patchedCustomResource = `
apiVersion: example.com/v1alpha1
kind: MyCRD
metadata:
  name: service
spec:
  template:
    spec:
      containers:
      - command: example
        image: nginx
        name: server
        ports:
        - containerPort: 8080
          name: grpc
          protocol: TCP
`

func runOpenApiTest(t *testing.T, test func(t *testing.T)) {
	t.Helper()
	openapi.ResetOpenAPI()
	test(t)
	openapi.ResetOpenAPI()
}

// Test for issue #2825
func TestCustomOpenApiFieldBasicUsage(t *testing.T) {
	runOpenApiTest(t, func(t *testing.T) {
		t.Helper()
		th := kusttest_test.MakeHarness(t)
		th.WriteK(".", `
resources:
- mycrd.yaml
openapi:
  path: mycrd_schema.json
`+customSchemaPatch)
		writeCustomResource(th, "mycrd.yaml")
		writeTestSchema(th, "./")
		m := th.Run(".", th.MakeDefaultOptions())
		th.AssertActualEqualsExpected(m, patchedCustomResource)
	})
}

func TestCustomOpenApiFieldBasicUsageWithRemoteSchema(t *testing.T) {
	runOpenApiTest(t, func(t *testing.T) {
		t.Helper()
		th := kusttest_test.MakeHarness(t)
		th.WriteK(".", `
resources:
- mycrd.yaml
openapi:
  path: https://github.com/kubernetes-sigs/kustomize/raw/master/api/krusty/testdata/customschema.json
`+customSchemaPatch)
		writeCustomResource(th, "mycrd.yaml")
		writeTestSchema(th, "./")
		m := th.Run(".", th.MakeDefaultOptions())
		th.AssertActualEqualsExpected(m, patchedCustomResource)
	})
}

func TestCustomOpenApiFieldWithTwoGvks(t *testing.T) {
	runOpenApiTest(t, func(t *testing.T) {
		t.Helper()
		th := kusttest_test.MakeHarness(t)
		th.WriteK(".", `
resources:
- mycrd.yaml
- myothercrd.yaml
openapi:
  path: mycrd_schema.json
`+customSchemaPatchMultipleGvks)
		writeCustomResource(th, "mycrd.yaml")
		writeOtherCustomResource(th, "myothercrd.yaml")
		writeTestSchema(th, "./")
		m := th.Run(".", th.MakeDefaultOptions())
		th.AssertActualEqualsExpected(m, `apiVersion: example.com/v1alpha1
kind: MyCRD
metadata:
  name: service
spec:
  template:
    spec:
      containers:
      - command: example
        image: nginx
        name: server
        ports:
        - containerPort: 8080
          name: grpc
          protocol: TCP
---
apiVersion: v1alpha1
kind: MyCRD
metadata:
  name: service
spec:
  template:
    spec:
      containers:
      - command: example
        image: nginx
        name: server
        ports:
        - containerPort: 8080
          name: grpc
          protocol: TCP
`)
	})
}

func TestCustomOpenApiFieldYaml(t *testing.T) {
	runOpenApiTest(t, func(t *testing.T) {
		t.Helper()
		th := kusttest_test.MakeHarness(t)
		th.WriteK(".", `
resources:
- mycrd.yaml
openapi:
  path: mycrd_schema.yaml
`+customSchemaPatch)
		writeCustomResource(th, "mycrd.yaml")
		writeTestSchemaYaml(th, "./")
		m := th.Run(".", th.MakeDefaultOptions())
		th.AssertActualEqualsExpected(m, patchedCustomResource)
	})
}

// Error if user tries to specify both builtin version
// and custom schema
func TestCustomOpenApiFieldBothPathAndVersion(t *testing.T) {
	runOpenApiTest(t, func(t *testing.T) {
		t.Helper()
		th := kusttest_test.MakeHarness(t)
		th.WriteK(".", `
resources:
- mycrd.yaml
openapi:
  version: v1.21.2
  path: mycrd_schema.json
`+customSchemaPatch)
		writeCustomResource(th, "mycrd.yaml")
		writeTestSchema(th, "./")
		err := th.RunWithErr(".", th.MakeDefaultOptions())
		require.Error(t, err)
		assert.Equal(t,
			"builtin version and custom schema provided, cannot use both",
			err.Error())
	})
}

// Test for if the filepath specified is not found
func TestCustomOpenApiFieldFileNotFound(t *testing.T) {
	runOpenApiTest(t, func(t *testing.T) {
		t.Helper()
		th := kusttest_test.MakeHarness(t)
		th.WriteK(".", `
resources:
- mycrd.yaml
openapi:
  path: mycrd_schema.json
`+customSchemaPatch)
		writeCustomResource(th, "mycrd.yaml")
		err := th.RunWithErr(".", th.MakeDefaultOptions())
		require.Error(t, err)
		assert.Equal(t,
			"'/mycrd_schema.json' doesn't exist",
			err.Error())
	})
}

func TestCustomOpenApiFieldFromBase(t *testing.T) {
	runOpenApiTest(t, func(t *testing.T) {
		t.Helper()
		th := kusttest_test.MakeHarness(t)
		th.WriteK("base", `
resources:
- mycrd.yaml
openapi:
  path: mycrd_schema.json
`)
		th.WriteK("overlay", `
resources:
- ../base
`+customSchemaPatch)
		writeCustomResource(th, "base/mycrd.yaml")
		writeTestSchema(th, "base/")
		m := th.Run("overlay", th.MakeDefaultOptions())
		th.AssertActualEqualsExpected(m, patchedCustomResource)
		assert.Equal(t, "using custom schema from file provided",
			openapi.GetSchemaVersion())
	})
}

func TestCustomOpenApiFieldFromBaseWithRemoteSchema(t *testing.T) {
	runOpenApiTest(t, func(t *testing.T) {
		t.Helper()
		th := kusttest_test.MakeHarness(t)
		th.WriteK("base", `
resources:
- mycrd.yaml
openapi:
  path: https://github.com/kubernetes-sigs/kustomize/raw/master/api/krusty/testdata/customschema.json
`)
		th.WriteK("overlay", `
resources:
- ../base
`+customSchemaPatch)
		writeCustomResource(th, "base/mycrd.yaml")
		writeTestSchema(th, "base/")
		m := th.Run("overlay", th.MakeDefaultOptions())
		th.AssertActualEqualsExpected(m, patchedCustomResource)
		assert.Equal(t, "using custom schema from file provided",
			openapi.GetSchemaVersion())
	})
}

func TestCustomOpenApiFieldFromOverlay(t *testing.T) {
	runOpenApiTest(t, func(t *testing.T) {
		t.Helper()
		th := kusttest_test.MakeHarness(t)
		th.WriteK("base", `
resources:
- mycrd.yaml
`)
		th.WriteK("overlay", `
resources:
- ../base
openapi:
  path: mycrd_schema.json
`+customSchemaPatch)
		writeCustomResource(th, "base/mycrd.yaml")
		writeTestSchema(th, "overlay/")
		m := th.Run("overlay", th.MakeDefaultOptions())
		th.AssertActualEqualsExpected(m, patchedCustomResource)
		assert.Equal(t, "using custom schema from file provided",
			openapi.GetSchemaVersion())
	})
}

func TestCustomOpenApiFieldFromOverlayWithRemoteSchema(t *testing.T) {
	runOpenApiTest(t, func(t *testing.T) {
		t.Helper()
		th := kusttest_test.MakeHarness(t)
		th.WriteK("base", `
resources:
- mycrd.yaml
`)
		th.WriteK("overlay", `
resources:
- ../base
openapi:
  path: https://github.com/kubernetes-sigs/kustomize/raw/master/api/krusty/testdata/customschema.json
`+customSchemaPatch)
		writeCustomResource(th, "base/mycrd.yaml")
		writeTestSchema(th, "overlay/")
		m := th.Run("overlay", th.MakeDefaultOptions())
		th.AssertActualEqualsExpected(m, patchedCustomResource)
		assert.Equal(t, "using custom schema from file provided",
			openapi.GetSchemaVersion())
	})
}

func TestCustomOpenApiFieldOverlayTakesPrecedence(t *testing.T) {
	runOpenApiTest(t, func(t *testing.T) {
		t.Helper()
		th := kusttest_test.MakeHarness(t)
		openapi.ResetOpenAPI()
		th.WriteK("base", `
resources:
- mycrd.yaml
openapi:
  path: mycrd_schema.json
`)
		th.WriteK("overlay", `
resources:
- ../base
openapi:
  version: v1.21.2
`+customSchemaPatch)
		writeCustomResource(th, "base/mycrd.yaml")
		writeTestSchema(th, "base/")
		m := th.Run("overlay", th.MakeDefaultOptions())
		th.AssertActualEqualsExpected(m, `
apiVersion: example.com/v1alpha1
kind: MyCRD
metadata:
  name: service
spec:
  template:
    spec:
      containers:
      - image: nginx
        name: server
`)
		assert.Equal(t, "v1.21.2", openapi.GetSchemaVersion())
	})
}

func TestCustomOpenApiFieldFromComponent(t *testing.T) {
	runOpenApiTest(t, func(t *testing.T) {
		t.Helper()
		input := []FileGen{
			writeTestBase,
			writeTestComponentWithCustomSchema,
			writeOverlayProd}

		th := kusttest_test.MakeHarness(t)
		for _, f := range input {
			f(th)
		}
		th.Run("prod", th.MakeDefaultOptions())
		assert.Equal(t, "using custom schema from file provided", openapi.GetSchemaVersion())
	})
}

// test for https://github.com/kubernetes-sigs/kustomize/issues/4179
// kustomize is not seeing the openapi field from the component defined in the overlay
func TestCustomOpenApiFieldFromComponentWithOverlays(t *testing.T) {
	runOpenApiTest(t, func(t *testing.T) {
		t.Helper()
		th := kusttest_test.MakeHarness(t)

		// overlay declaring the component
		th.WriteK("overlays/overlay-component-openapi", `resources:
- ../base/
components:
- ../../components/dc-openapi
`)

		// base kustomization
		th.WriteK("overlays/base", `resources:
- dc.yml
`)

		// resource declared in the base kustomization
		th.WriteF("overlays/base/dc.yml", `apiVersion: apps.openshift.io/v1
kind: DeploymentConfig
metadata:
  name: my-dc
spec:
  template:
    spec:
      initContainers:
        - name: init
      containers:
        - name: container
          env:
            - name: foo
              value: bar
          volumeMounts:
            - name: cm
              mountPath: /opt/cm
      volumes:
        - name: cm
          configMap:
            name: cm
`)

		// openapi schema referred to by the component
		bytes, _ := os.ReadFile("./testdata/openshiftschema.json")
		th.WriteF("components/dc-openapi/openapi.json", string(bytes))

		// patch referred to by the component
		th.WriteF("components/dc-openapi/patch.yml", `apiVersion: apps.openshift.io/v1
kind: DeploymentConfig
metadata:
  name: my-dc
spec:
  template:
    spec:
      containers:
        - name: container
          volumeMounts:
            - name: additional-cm
              mountPath: /mnt
      volumes:
        - name: additional-cm
          configMap:
             name: additional-cm
`)

		// component declared in overlay with custom schema and patch
		th.WriteC("components/dc-openapi", `patchesStrategicMerge:
  - patch.yml
openapi:
  path: openapi.json
`)

		m := th.Run("overlays/overlay-component-openapi", th.MakeDefaultOptions())
		th.AssertActualEqualsExpected(m, `apiVersion: apps.openshift.io/v1
kind: DeploymentConfig
metadata:
  name: my-dc
spec:
  template:
    spec:
      containers:
      - env:
        - name: foo
          value: bar
        name: container
        volumeMounts:
        - mountPath: /mnt
          name: additional-cm
        - mountPath: /opt/cm
          name: cm
      initContainers:
      - name: init
      volumes:
      - configMap:
          name: additional-cm
        name: additional-cm
      - configMap:
          name: cm
        name: cm
`)
	})
}

func TestCustomOpenApiFieldVersion(t *testing.T) {
	runOpenApiTest(t, func(t *testing.T) {
		t.Helper()
		th := kusttest_test.MakeHarness(t)
		th.WriteK(".", `
openapi:
  version: v1.21.2
resources:
- deployment.yaml
`)
		th.WriteF("deployment.yaml", `
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myDeployment
spec:
  template:
    spec:
      containers:
      - image: whatever
`)

		m := th.Run(".", th.MakeDefaultOptions())
		th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myDeployment
spec:
  template:
    spec:
      containers:
      - image: whatever
`)
		assert.Equal(t, "v1.21.2", openapi.GetSchemaVersion())
	})
}

func TestCustomOpenApiFieldNotBuiltin(t *testing.T) {
	runOpenApiTest(t, func(t *testing.T) {
		t.Helper()
		th := kusttest_test.MakeHarness(t)
		th.WriteK(".", `
openapi:
  version: v1.14.1
resources:
- deployment.yaml
`)
		th.WriteF("deployment.yaml", `
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myDeployment
spec:
  template:
    spec:
      containers:
      - image: whatever
`)

		err := th.RunWithErr(".", th.MakeDefaultOptions())
		if err == nil {
			t.Fatalf("expected an error")
		}
	})
}

func TestCustomOpenApiFieldDefaultVersion(t *testing.T) {
	runOpenApiTest(t, func(t *testing.T) {
		t.Helper()
		th := kusttest_test.MakeHarness(t)
		th.WriteK(".", `
resources:
- deployment.yaml
`)
		th.WriteF("deployment.yaml", `
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myDeployment
spec:
  template:
    spec:
      containers:
      - image: whatever
`)

		m := th.Run(".", th.MakeDefaultOptions())
		th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myDeployment
spec:
  template:
    spec:
      containers:
      - image: whatever
`)
		assert.Equal(t, kubernetesapi.DefaultOpenAPI, openapi.GetSchemaVersion())
	})
}
