/*
Copyright 2022 The Flux authors

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"
	"strings"
	"testing"

	. "github.com/onsi/gomega"
)

func Test_saveReaderToFile(t *testing.T) {
	g := NewWithT(t)

	testString := `apiVersion: v1
kind: ConfigMap
metadata:
  name: myapp
data:
  foo: bar`

	tests := []struct {
		name      string
		string    string
		expectErr bool
	}{
		{
			name:   "yaml",
			string: testString,
		},
		{
			name:   "yaml with carriage return",
			string: testString + "\r\n",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tmpFile, err := saveReaderToFile(strings.NewReader(tt.string))
			g.Expect(err).To(BeNil())

			t.Cleanup(func() { _ = os.Remove(tmpFile) })

			b, err := os.ReadFile(tmpFile)
			if tt.expectErr {
				g.Expect(err).To(Not(BeNil()))
				return
			}

			g.Expect(err).To(BeNil())
			g.Expect(string(b)).To(BeEquivalentTo(testString))
		})

	}
}

func Test_resolveSymlinks(t *testing.T) {
	g := NewWithT(t)

	// Create source directory with a real file
	srcDir := t.TempDir()
	realFile := filepath.Join(srcDir, "real.yaml")
	g.Expect(os.WriteFile(realFile, []byte("apiVersion: v1\nkind: Namespace\nmetadata:\n  name: test\n"), 0o644)).To(Succeed())

	// Create a directory with symlinks pointing to files outside it
	symlinkDir := t.TempDir()
	symlinkFile := filepath.Join(symlinkDir, "linked.yaml")
	g.Expect(os.Symlink(realFile, symlinkFile)).To(Succeed())

	// Also add a regular file in the symlink dir
	regularFile := filepath.Join(symlinkDir, "regular.yaml")
	g.Expect(os.WriteFile(regularFile, []byte("apiVersion: v1\nkind: ConfigMap\n"), 0o644)).To(Succeed())

	// Create a symlinked subdirectory
	subDir := filepath.Join(srcDir, "subdir")
	g.Expect(os.MkdirAll(subDir, 0o755)).To(Succeed())
	g.Expect(os.WriteFile(filepath.Join(subDir, "nested.yaml"), []byte("nested"), 0o644)).To(Succeed())
	g.Expect(os.Symlink(subDir, filepath.Join(symlinkDir, "linkeddir"))).To(Succeed())

	// Resolve symlinks
	resolved, cleanupDir, err := resolveSymlinks(symlinkDir)
	g.Expect(err).To(BeNil())
	t.Cleanup(func() { os.RemoveAll(cleanupDir) })

	// Verify the regular file was copied
	content, err := os.ReadFile(filepath.Join(resolved, "regular.yaml"))
	g.Expect(err).To(BeNil())
	g.Expect(string(content)).To(Equal("apiVersion: v1\nkind: ConfigMap\n"))

	// Verify the symlinked file was resolved and copied
	content, err = os.ReadFile(filepath.Join(resolved, "linked.yaml"))
	g.Expect(err).To(BeNil())
	g.Expect(string(content)).To(ContainSubstring("kind: Namespace"))

	// Verify that the resolved file is a regular file, not a symlink
	info, err := os.Lstat(filepath.Join(resolved, "linked.yaml"))
	g.Expect(err).To(BeNil())
	g.Expect(info.Mode().IsRegular()).To(BeTrue())

	// Verify that the symlinked directory was resolved and its contents were copied
	content, err = os.ReadFile(filepath.Join(resolved, "linkeddir", "nested.yaml"))
	g.Expect(err).To(BeNil())
	g.Expect(string(content)).To(Equal("nested"))

	// Verify that the file inside the symlinked directory is a regular file
	info, err = os.Lstat(filepath.Join(resolved, "linkeddir", "nested.yaml"))
	g.Expect(err).To(BeNil())
	g.Expect(info.Mode().IsRegular()).To(BeTrue())
}

func Test_resolveSymlinks_singleFile(t *testing.T) {
	g := NewWithT(t)

	// Create a real file
	srcDir := t.TempDir()
	realFile := filepath.Join(srcDir, "manifest.yaml")
	g.Expect(os.WriteFile(realFile, []byte("kind: ConfigMap"), 0o644)).To(Succeed())

	// Create a symlink to the real file
	linkDir := t.TempDir()
	linkFile := filepath.Join(linkDir, "link.yaml")
	g.Expect(os.Symlink(realFile, linkFile)).To(Succeed())

	// Resolve the single symlinked file
	resolved, cleanupDir, err := resolveSymlinks(linkFile)
	g.Expect(err).To(BeNil())
	t.Cleanup(func() { os.RemoveAll(cleanupDir) })

	// The returned path should be a file, not a directory
	info, err := os.Stat(resolved)
	g.Expect(err).To(BeNil())
	g.Expect(info.IsDir()).To(BeFalse())

	// Verify contents
	content, err := os.ReadFile(resolved)
	g.Expect(err).To(BeNil())
	g.Expect(string(content)).To(Equal("kind: ConfigMap"))
}

func Test_resolveSymlinks_cycle(t *testing.T) {
	g := NewWithT(t)

	// Create a directory with a symlink cycle: dir/link -> dir
	dir := t.TempDir()
	g.Expect(os.WriteFile(filepath.Join(dir, "file.yaml"), []byte("data"), 0o644)).To(Succeed())
	g.Expect(os.Symlink(dir, filepath.Join(dir, "cycle"))).To(Succeed())

	// resolveSymlinks should not infinite-loop
	resolved, cleanupDir, err := resolveSymlinks(dir)
	g.Expect(err).To(BeNil())
	t.Cleanup(func() { os.RemoveAll(cleanupDir) })

	// The file should be copied
	content, err := os.ReadFile(filepath.Join(resolved, "file.yaml"))
	g.Expect(err).To(BeNil())
	g.Expect(string(content)).To(Equal("data"))

	// The cycle directory should exist but not cause infinite nesting
	_, err = os.Stat(filepath.Join(resolved, "cycle"))
	g.Expect(err).To(BeNil())

	// There should NOT be deeply nested cycle/cycle/cycle/... paths
	_, err = os.Stat(filepath.Join(resolved, "cycle", "cycle", "cycle"))
	g.Expect(os.IsNotExist(err)).To(BeTrue())
}

func Test_composeIgnorePaths(t *testing.T) {
	tests := []struct {
		name           string
		ignorePaths    []string
		addIgnorePaths []string
		want           []string
	}{
		{
			name:           "both nil returns nil",
			ignorePaths:    nil,
			addIgnorePaths: nil,
			want:           nil,
		},
		{
			name:           "only ignore-paths passes through",
			ignorePaths:    []string{"foo", "bar"},
			addIgnorePaths: nil,
			want:           []string{"foo", "bar"},
		},
		{
			name:           "only add-ignore-paths passes through",
			ignorePaths:    nil,
			addIgnorePaths: []string{"baz"},
			want:           []string{"baz"},
		},
		{
			name:           "both provided, order preserved",
			ignorePaths:    []string{"foo", "bar"},
			addIgnorePaths: []string{"baz", "qux"},
			want:           []string{"foo", "bar", "baz", "qux"},
		},
		{
			name:           "duplicates retained as-is",
			ignorePaths:    []string{"foo", "bar"},
			addIgnorePaths: []string{"foo"},
			want:           []string{"foo", "bar", "foo"},
		},
		{
			name:           "defaults plus add",
			ignorePaths:    excludeOCI,
			addIgnorePaths: []string{"my-secrets/"},
			want:           append(append([]string{}, excludeOCI...), "my-secrets/"),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			g := NewWithT(t)
			got := composeIgnorePaths(tt.ignorePaths, tt.addIgnorePaths)
			g.Expect(got).To(Equal(tt.want))
		})
	}
}

func Test_resolveSymlinks_multipleLinksSameTarget(t *testing.T) {
	g := NewWithT(t)

	// Create source directory with a real file inside a dir
	srcDir := t.TempDir()
	targetDir := filepath.Join(srcDir, "target")
	g.Expect(os.MkdirAll(targetDir, 0o755)).To(Succeed())
	g.Expect(os.WriteFile(filepath.Join(targetDir, "file.yaml"), []byte("data"), 0o644)).To(Succeed())

	// Create a directory with multiple symlinks pointing to targetDir
	symlinkDir := t.TempDir()

	// Link 1
	link1 := filepath.Join(symlinkDir, "link1")
	g.Expect(os.Symlink(targetDir, link1)).To(Succeed())

	// Link 2
	link2 := filepath.Join(symlinkDir, "link2")
	g.Expect(os.Symlink(targetDir, link2)).To(Succeed())

	// Resolve symlinks
	resolved, cleanupDir, err := resolveSymlinks(symlinkDir)
	g.Expect(err).To(BeNil())
	t.Cleanup(func() { os.RemoveAll(cleanupDir) })

	// Verify link1 has the file
	content, err := os.ReadFile(filepath.Join(resolved, "link1", "file.yaml"))
	g.Expect(err).To(BeNil())
	g.Expect(string(content)).To(Equal("data"))

	// Verify link2 ALSO has the file
	content2, err := os.ReadFile(filepath.Join(resolved, "link2", "file.yaml"))
	g.Expect(err).To(BeNil())
	g.Expect(string(content2)).To(Equal("data"))
}
