//  Copyright 2024 Google LLC
//
//  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
//
//     https://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 file

import (
	"archive/tar"
	"compress/gzip"
	"context"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
)

func TestFileExistsSuccess(t *testing.T) {
	file := filepath.Join(t.TempDir(), "test.txt")
	if err := os.WriteFile(file, []byte("test"), 0644); err != nil {
		t.Fatalf("Failed to write test file: %v", err)
	}

	if !Exists(file, TypeFile) {
		t.Errorf("Exists(%s) = false, want true", file)
	}

	if !Exists(t.TempDir(), TypeDir) {
		t.Errorf("Exists(%s) = false, want true", t.TempDir())
	}
}

func TestFileExistsFailure(t *testing.T) {
	file := "/proc/unknown"
	if Exists(file, TypeFile) {
		t.Errorf("Exists(%s, TypeFile) = true, want false", file)
	}

	if Exists(file, TypeDir) {
		t.Errorf("Exists(%s, TypeDir) = true, want false", file)
	}
}

func TestSha256Sum(t *testing.T) {
	file := filepath.Join(t.TempDir(), "test.txt")
	if err := os.WriteFile(file, []byte("test"), 0644); err != nil {
		t.Fatalf("Failed to write test file: %v", err)
	}

	tests := []struct {
		name      string
		wantCksum string
		path      string
		wantErr   bool
	}{
		{
			name:      "valid_file",
			wantCksum: "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
			wantErr:   false,
			path:      file,
		},
		{
			name:    "invalid_file",
			wantErr: true,
			path:    filepath.Join(t.TempDir(), "random-non-existent-file"),
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			got, err := SHA256FileSum(test.path)
			if (err != nil) != test.wantErr {
				t.Errorf("Sha256Sum(%s) want error return: %t, got error: %v", test.path, test.wantErr, err)
			}
			if got != test.wantCksum {
				t.Errorf("Sha256Sum(%s) = %s, want %s", test.path, got, test.wantCksum)
			}
		})
	}
}

const (
	dirPerms  = 0755
	filePerms = 0644
)

// createTestArchive creates a test archive file of following structure.
// └── dir1
// │   └── file1
// │   └── dir2
// └── file2 -> dir1/file1
func createTestArchive(t *testing.T) string {
	t.Helper()

	tempDir := t.TempDir()
	tempFile := filepath.Join(tempDir, "test.tar.gz")
	f, err := os.Create(tempFile)
	if err != nil {
		t.Fatalf("error creating test file: %v", err)
	}

	gw := gzip.NewWriter(f)
	tw := tar.NewWriter(gw)

	dir1 := "dir1"
	if err := tw.WriteHeader(&tar.Header{Name: dir1, Mode: dirPerms, Typeflag: tar.TypeDir}); err != nil {
		t.Fatalf("error creating test archive header: %v", err)
	}

	name := filepath.Join("dir1", "file1")
	body := "test content"

	if err := tw.WriteHeader(&tar.Header{Name: name, Mode: filePerms, Size: int64(len(body))}); err != nil {
		t.Fatalf("error creating test archive header: %v", err)
	}

	if _, err := tw.Write([]byte(body)); err != nil {
		t.Fatalf("error writing test archive content: %v", err)
	}

	dir2 := filepath.Join("dir1", "dir2")
	if err := tw.WriteHeader(&tar.Header{Name: dir2, Mode: dirPerms, Typeflag: tar.TypeDir}); err != nil {
		t.Fatalf("error creating test archive header: %v", err)
	}

	link := "file2"
	h := &tar.Header{Name: link, Mode: filePerms, Typeflag: tar.TypeSymlink, Linkname: name}
	if err := tw.WriteHeader(h); err != nil {
		t.Fatalf("error creating test archive header: %v", err)
	}

	if err := tw.Close(); err != nil {
		t.Fatalf("error closing test archive tar writer: %v", err)
	}
	if err := gw.Close(); err != nil {
		t.Fatalf("error closing test archive gzip writer: %v", err)
	}
	if err := f.Close(); err != nil {
		t.Fatalf("error closing test archive file: %v", err)
	}

	return tempFile
}

type testfile struct {
	path    string
	content string
}

func TestUnpackTARGZFile(t *testing.T) {
	srcArchive := createTestArchive(t)
	destDir := t.TempDir()

	if err := UnpackTargzFile(srcArchive, destDir); err != nil {
		t.Fatalf("UnpackTARGZFile(%s, %s) failed unexpectedly with error: %v", srcArchive, destDir, err)
	}

	wantDirs := []string{filepath.Join(destDir, "dir1"), filepath.Join(destDir, "dir1", "dir2")}
	wantFiles := []testfile{testfile{filepath.Join(destDir, "dir1", "file1"), "test content"}, testfile{filepath.Join(destDir, "file2"), "test content"}}
	wantSymlinks := []string{filepath.Join(destDir, "file2")}
	wantDirPerms := os.FileMode(dirPerms)
	wantFilePerms := os.FileMode(filePerms)

	// File permissions on Windows work differently than unix permissions. File
	// mode returns same permission for all owner/group/other as that is set for
	// owner.
	if runtime.GOOS == "windows" {
		wantDirPerms = os.FileMode(0777)
		wantFilePerms = os.FileMode(0666)
	}

	// Verify all files and the content exists as expected.
	for _, wantFile := range wantFiles {
		info, err := os.Stat(wantFile.path)
		if err != nil {
			t.Errorf("os.Stat(%s) failed unexpectedly with error: %v", wantFile.path, err)
		}
		if info.IsDir() {
			t.Errorf("%s should be a regular file, found to be a directory", wantFile.path)
		}
		if info.Mode().Perm() != wantFilePerms {
			t.Errorf("os.Stat(%s) = %v, want %v", wantFile.path, info.Mode().Perm(), wantFilePerms)
		}
		gotContent, err := os.ReadFile(wantFile.path)
		if err != nil {
			t.Errorf("os.ReadFile(%s) failed unexpectedly with error: %v", wantFile.path, err)
		}
		if string(gotContent) != wantFile.content {
			t.Errorf("os.ReadFile(%s) = %s, want %s", wantFile.path, string(gotContent), wantFile.content)
		}
	}

	// Verify all the directories were created as expected.
	for _, wantDir := range wantDirs {
		info, err := os.Stat(wantDir)
		if err != nil {
			t.Errorf("os.Stat(%s) failed unexpectedly with error: %v", wantDir, err)
		}
		if !info.IsDir() {
			t.Errorf("os.Stat(%s) = %v, want %v", wantDir, info.Mode(), os.ModeDir)
		}
		if info.Mode().Perm() != wantDirPerms {
			t.Errorf("os.Stat(%s) = %v, want %v", wantDir, info.Mode().Perm(), wantDirPerms)
		}
	}

	// Verify all the symlinks were created as expected.
	for _, wantSymlink := range wantSymlinks {
		fileInfo, err := os.Lstat(wantSymlink)
		if err != nil {
			t.Errorf("os.Lstat(%s) failed unexpectedly with error: %v", wantSymlink, err)
		}

		if fileInfo.Mode()&os.ModeSymlink == 0 {
			t.Errorf("%s should have been a symlink, got %v", wantSymlink, fileInfo.Mode())
		}
	}
}

func TestSaferWriteFile(t *testing.T) {
	f := filepath.Join(t.TempDir(), "file")
	want := "test-data"

	if err := SaferWriteFile(context.Background(), []byte(want), f, Options{Perm: 0644}); err != nil {
		t.Errorf("SaferWriteFile(%s, %s) failed unexpectedly with err: %+v", "test-data", f, err)
	}

	got, err := os.ReadFile(f)
	if err != nil {
		t.Errorf("os.ReadFile(%s) failed unexpectedly with err: %+v", f, err)
	}
	if string(got) != want {
		t.Errorf("os.ReadFile(%s) = %s, want %s", f, string(got), want)
	}

	i, err := os.Stat(f)
	if err != nil {
		t.Errorf("os.Stat(%s) failed unexpectedly with err: %+v", f, err)
	}

	// Windows will always return 0666 for all files.
	if runtime.GOOS != "windows" && i.Mode().Perm() != 0644 {
		t.Errorf("SaferWriteFile(%s) set incorrect permissions, os.Stat(%s) = %o, want %o", f, f, i.Mode().Perm(), 0o644)
	}
}

func TestCopyFile(t *testing.T) {
	tmp := t.TempDir()
	dst := filepath.Join(tmp, "dst")
	src := filepath.Join(tmp, "src")
	want := "testdata"

	if err := os.WriteFile(src, []byte(want), 0777); err != nil {
		t.Fatalf("failed to write test source file: %v", err)
	}
	if err := CopyFile(context.Background(), src, dst, Options{Perm: 0644}); err != nil {
		t.Errorf("CopyFile(%s, %s) failed unexpectedly with error: %v", src, dst, err)
	}

	got, err := os.ReadFile(dst)
	if err != nil {
		t.Errorf("unable to read %q: %v", dst, err)
	}
	if string(got) != want {
		t.Errorf("CopyFile(%s, %s) copied %q, expected %q", src, dst, string(got), want)
	}

	i, err := os.Stat(dst)
	if err != nil {
		t.Errorf("os.Stat(%s) failed unexpectedly with err: %+v", dst, err)
	}

	// Windows will always return 0666 for all files.
	if runtime.GOOS != "windows" && i.Mode().Perm() != 0644 {
		t.Errorf("SaferWriteFile(%s) set incorrect permissions, os.Stat(%s) = %o, want %o", dst, dst, i.Mode().Perm(), 0644)
	}
}

func TestCopyFileError(t *testing.T) {
	tmp := t.TempDir()
	dst := filepath.Join(tmp, "dst")
	src := filepath.Join(tmp, "src")

	if err := CopyFile(context.Background(), src, dst, Options{Perm: 0644}); err == nil {
		t.Errorf("CopyFile(%s, %s) succeeded for non-existent file, want error", src, dst)
	}
}

func TestReadLastNLines(t *testing.T) {
	tmp := t.TempDir()
	file := filepath.Join(tmp, "file")
	write := []string{"line 1", "line 2", "line 3", strings.Repeat("a", 1024)}

	if err := os.WriteFile(file, []byte(strings.Join(write, "\n")), 0644); err != nil {
		t.Fatalf("Failed to write test file: %v", err)
	}

	file2 := filepath.Join(tmp, "file2")
	f, err := os.Create(file2)
	if err != nil {
		t.Fatalf("Failed to create test file: %v", err)
	}
	if err := f.Close(); err != nil {
		t.Fatalf("Failed to close test file: %v", err)
	}

	file3 := filepath.Join(tmp, "file3")
	if err := os.WriteFile(file3, []byte("single line without newline"), 0644); err != nil {
		t.Fatalf("Failed to write test file: %v", err)
	}

	tests := []struct {
		name  string
		file  string
		lines int
		want  []string
	}{
		{
			name:  "read_1_line",
			file:  file,
			lines: 1,
			want:  []string{write[3]},
		},
		{
			name:  "read_2_lines",
			file:  file,
			lines: 2,
			want:  []string{write[2], write[3]},
		},
		{
			name:  "read_3_lines",
			file:  file,
			lines: 3,
			want:  []string{write[1], write[2], write[3]},
		},
		{
			name:  "read_10_lines",
			file:  file,
			lines: 10,
			want:  []string{write[0], write[1], write[2], write[3]},
		},
		{
			name:  "read_0_lines",
			file:  file,
			lines: 0,
			want:  []string{},
		},
		{
			name:  "empty_file",
			file:  file2,
			lines: 10,
		},
		{
			name:  "empty_file_0_lines",
			file:  file2,
			lines: 0,
		},
		{
			name:  "single_line_no_newline",
			file:  file3,
			lines: 1,
			want:  []string{"single line without newline"},
		},
		{
			name:  "single_line_no_newline_request_more",
			file:  file3,
			lines: 5,
			want:  []string{"single line without newline"},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			got, err := ReadLastNLines(test.file, test.lines)
			if err != nil {
				t.Errorf("ReadLastNLines(%s, %d) failed unexpectedly with error: %v", test.file, test.lines, err)
			}
			if diff := cmp.Diff(test.want, got); diff != "" {
				t.Errorf("ReadLastNLines(%s, %d) returned diff (-want +got):\n%s", test.file, test.lines, diff)
			}
		})
	}
}

func TestUpdateSymlink(t *testing.T) {
	target2 := t.TempDir()
	target3 := t.TempDir()

	sym2 := filepath.Join(t.TempDir(), "sym2")
	if err := os.Symlink(target2, sym2); err != nil {
		t.Fatalf("Failed to setup test symlink: %v", err)
	}

	sym3 := filepath.Join(t.TempDir(), "sym3")
	if err := os.Symlink(target3, sym3); err != nil {
		t.Fatalf("Failed to setup test symlink: %v", err)
	}

	tests := []struct {
		name    string
		target  string
		symlink string
	}{
		{
			name:    "non_existing_symlink",
			target:  t.TempDir(),
			symlink: filepath.Join(t.TempDir(), "sym1"),
		},
		{
			name:    "update_symlink",
			target:  filepath.Join(t.TempDir(), "target2_new"),
			symlink: sym2,
		},
		{
			name:    "no_op_symlink",
			target:  target3,
			symlink: sym3,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if err := UpdateSymlink(test.symlink, test.target); err != nil {
				t.Errorf("UpdateSymlink(%s, %s) failed unexpectedly with error: %v", test.symlink, test.target, err)
			}
			got, err := os.Readlink(test.symlink)
			if err != nil {
				t.Fatalf("os.Readlink(%s) failed unexpectedly with error: %v", test.symlink, err)
			}
			if got != test.target {
				t.Errorf("UpdateSymlink(%s, %s) target = %s, want %s", test.symlink, test.target, got, test.target)
			}
		})
	}
}
