// Copyright (c) 2026 Tigera, Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
	"os"
	"path/filepath"
	"testing"
)

func TestDocumentSet(t *testing.T) {
	t.Parallel()

	const doc = `metadata:
  annotations:
    createdAt: "2020-01-01T00:00:00Z"
spec:
  displayName: Tigera Operator
  install:
    spec:
      deployments:
        - spec:
            template:
              spec:
                containers:
                  - name: tigera-operator
                    image: quay.io/tigera/operator:v1.0.0
      permissions:
        - serviceAccountName: tigera-operator
`

	cases := []struct {
		name  string
		path  []any
		value string
		want  string
	}{
		{
			name:  "overwrites an existing key",
			path:  []any{"metadata", "annotations", "createdAt"},
			value: "2026-01-01T00:00:00Z",
			want: `metadata:
  annotations:
    createdAt: "2026-01-01T00:00:00Z"
`,
		},
		{
			name:  "adds a key to an existing mapping",
			path:  []any{"metadata", "annotations", "olm.skipRange"},
			value: "<3.34.0",
			want: `    createdAt: "2020-01-01T00:00:00Z"
    olm.skipRange: <3.34.0
`,
		},
		{
			name:  "creates a missing mapping",
			path:  []any{"metadata", "labels", "operatorframework.io/arch.arm64"},
			value: "supported",
			want: `  labels:
    operatorframework.io/arch.arm64: supported
`,
		},
		{
			name:  "creates a missing sequence",
			path:  []any{"spec", "relatedImages", 0, "name"},
			value: "tigera-operator",
			want: `  relatedImages:
    - name: tigera-operator
`,
		},
		{
			name:  "walks into a sequence",
			path:  []any{"spec", "install", "spec", "deployments", 0, "spec", "template", "spec", "containers", 0, "image"},
			value: "quay.io/tigera/operator@sha256:abc",
			want:  `                    image: quay.io/tigera/operator@sha256:abc`,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			d := writeDocument(t, doc)
			if err := d.set(tc.value, tc.path...); err != nil {
				t.Fatalf("set(%v): %v", tc.path, err)
			}
			if err := d.save(); err != nil {
				t.Fatalf("save: %v", err)
			}
			assertContains(t, readFile(t, d.path), tc.want)
		})
	}
}

// TestDocumentMustSet covers the difference from set: a path that is not
// already there is an error rather than something to create.
func TestDocumentMustSet(t *testing.T) {
	t.Parallel()

	const doc = `metadata:
  annotations:
    createdAt: "2020-01-01T00:00:00Z"
spec:
  install:
    spec:
      deployments:
        - spec:
            template:
              spec:
                containers:
                  - name: tigera-operator
                    image: quay.io/tigera/operator:v1.0.0
`

	deploymentImage := []any{"spec", "install", "spec", "deployments", 0, "spec", "template", "spec", "containers", 0, "image"}

	t.Run("overwrites a path that is there", func(t *testing.T) {
		t.Parallel()

		d := writeDocument(t, doc)
		if err := d.mustSet("quay.io/tigera/operator@sha256:abc", deploymentImage...); err != nil {
			t.Fatalf("mustSet: %v", err)
		}
		if err := d.save(); err != nil {
			t.Fatalf("save: %v", err)
		}
		assertContains(t, readFile(t, d.path), "image: quay.io/tigera/operator@sha256:abc")
	})

	cases := []struct {
		name string
		doc  string
		path []any
	}{
		{
			name: "a missing leaf key",
			doc:  doc,
			path: []any{"metadata", "annotations", "olm.skipRange"},
		},
		{
			name: "a missing mapping along the path",
			doc:  doc,
			path: []any{"metadata", "labels", "operatorframework.io/arch.arm64"},
		},
		{
			name: "a missing sequence along the path",
			doc:  doc,
			path: []any{"spec", "relatedImages", 0, "name"},
		},
		{
			name: "a null collection along the path",
			doc: `spec:
  install:
    spec:
      deployments:
`,
			path: deploymentImage,
		},
		{
			name: "a collection along the path that is not there",
			doc:  "spec:\n  install: {}\n",
			path: deploymentImage,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			d := writeDocument(t, tc.doc)
			if err := d.mustSet("value", tc.path...); err == nil {
				t.Fatalf("mustSet(%v) succeeded, want an error", tc.path)
			}
		})
	}
}

func TestDocumentFind(t *testing.T) {
	t.Parallel()

	const doc = `spec:
  containers:
    - name: sidecar
      image: quay.io/tigera/sidecar
    - name: tigera-operator
      image: quay.io/tigera/operator
`

	containers := []any{"spec", "containers"}

	t.Run("finds an element by name", func(t *testing.T) {
		t.Parallel()

		d := writeDocument(t, doc)
		i, err := d.find(containers, "name", "tigera-operator")
		if err != nil {
			t.Fatalf("find: %v", err)
		}
		if i != 1 {
			t.Errorf("index is %d, want 1", i)
		}
	})

	t.Run("findOrNext finds an element by name", func(t *testing.T) {
		t.Parallel()

		d := writeDocument(t, doc)
		i, err := d.findOrNext(containers, "name", "tigera-operator")
		if err != nil {
			t.Fatalf("findOrNext: %v", err)
		}
		if i != 1 {
			t.Errorf("index is %d, want 1", i)
		}
	})

	t.Run("findOrNext appends past the end", func(t *testing.T) {
		t.Parallel()

		for _, tc := range []struct {
			doc  string
			want int
		}{
			{doc: doc, want: 2},
			{doc: "spec:\n  containers:\n", want: 0},
			{doc: "spec: {}\n", want: 0},
		} {
			d := writeDocument(t, tc.doc)
			i, err := d.findOrNext(containers, "name", "typha")
			if err != nil {
				t.Fatalf("findOrNext(%q): %v", tc.doc, err)
			}
			if i != tc.want {
				t.Errorf("findOrNext(%q) is %d, want %d", tc.doc, i, tc.want)
			}
		}
	})

	t.Run("findOrNext rejects a non-sequence", func(t *testing.T) {
		t.Parallel()

		d := writeDocument(t, "spec:\n  containers: {}\n")
		if _, err := d.findOrNext(containers, "name", "typha"); err == nil {
			t.Fatalf("findOrNext succeeded, want an error")
		}
	})

	cases := []struct {
		name  string
		doc   string
		path  []any
		value string
	}{
		{name: "no element with that name", doc: doc, path: containers, value: "typha"},
		{name: "the sequence is not there", doc: doc, path: []any{"spec", "volumes"}, value: "var-lib"},
		{name: "the sequence is null", doc: "spec:\n  containers:\n", path: containers, value: "tigera-operator"},
		{name: "not a sequence", doc: "spec:\n  containers: {}\n", path: containers, value: "tigera-operator"},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			d := writeDocument(t, tc.doc)
			if _, err := d.find(tc.path, "name", tc.value); err == nil {
				t.Fatalf("find(%v, %q) succeeded, want an error", tc.path, tc.value)
			}
		})
	}
}

func TestDocumentSetErrors(t *testing.T) {
	t.Parallel()

	const doc = `spec:
  displayName: Tigera Operator
  relatedImages:
    - name: tigera-operator
`

	cases := []struct {
		name string
		path []any
	}{
		{name: "no path", path: nil},
		{name: "index into a scalar", path: []any{"spec", "displayName", 0, "name"}},
		{name: "key of a sequence", path: []any{"spec", "relatedImages", "name"}},
		{name: "index past the end", path: []any{"spec", "relatedImages", 2, "name"}},
		{name: "sequence index last", path: []any{"spec", "relatedImages", 0}},
		{name: "overwrite a mapping", path: []any{"spec"}},
		{name: "overwrite a sequence", path: []any{"spec", "relatedImages"}},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			d := writeDocument(t, doc)
			if err := d.set("value", tc.path...); err == nil {
				t.Fatalf("set(%v) succeeded, want an error", tc.path)
			}
		})
	}
}

func TestDocumentDelete(t *testing.T) {
	t.Parallel()

	const doc = `spec:
  install:
    spec:
      permissions:
        - serviceAccountName: tigera-operator
      clusterPermissions:
        - serviceAccountName: tigera-operator
`

	cases := []struct {
		name    string
		path    []any
		absent  string
		present string
	}{
		{
			name:    "deletes a key",
			path:    []any{"spec", "install", "spec", "permissions"},
			absent:  "      permissions:",
			present: "      clusterPermissions:",
		},
		{
			name:    "a missing key is not an error",
			path:    []any{"spec", "install", "spec", "webhookdefinitions"},
			present: "      permissions:",
		},
		{
			name:    "a missing parent is not an error",
			path:    []any{"spec", "customresourcedefinitions", "owned"},
			present: "      permissions:",
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			d := writeDocument(t, doc)
			if err := d.delete(tc.path...); err != nil {
				t.Fatalf("delete(%v): %v", tc.path, err)
			}
			if err := d.save(); err != nil {
				t.Fatalf("save: %v", err)
			}
			content := readFile(t, d.path)
			if tc.absent != "" {
				assertNotContains(t, content, tc.absent)
			}
			assertContains(t, content, tc.present)
		})
	}
}

// TestDocumentPreservesTheRest checks that an update rewrites the fields it was
// asked for and leaves the rest of the document - key order, comments and
// scalar styles - as it found it.
func TestDocumentPreservesTheRest(t *testing.T) {
	t.Parallel()

	const doc = `# A leading comment.
spec:
  displayName: Tigera Operator
  description: |-
    A literal block
    over two lines.
  version: 0.0.0
`

	d := writeDocument(t, doc)
	if err := d.set("Tigera Operator v3.34", "spec", "displayName"); err != nil {
		t.Fatalf("set: %v", err)
	}
	if err := d.save(); err != nil {
		t.Fatalf("save: %v", err)
	}

	want := `# A leading comment.
spec:
  displayName: Tigera Operator v3.34
  description: |-
    A literal block
    over two lines.
  version: 0.0.0
`
	if got := readFile(t, d.path); got != want {
		t.Errorf("document is\n%s\nwant\n%s", got, want)
	}
}

func writeDocument(t *testing.T, content string) *document {
	t.Helper()

	path := filepath.Join(t.TempDir(), "doc.yaml")
	if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
		t.Fatalf("writing %s: %v", path, err)
	}
	d, err := loadDocument(path)
	if err != nil {
		t.Fatalf("loading %s: %v", path, err)
	}
	return d
}
