// Copyright 2015 Matthew Holt and The Caddy 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.

//go:build !windows

package logging

import (
	"encoding/json"
	"os"
	"path"
	"path/filepath"
	"syscall"
	"testing"
	"time"

	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)

func TestFileCreationMode(t *testing.T) {
	on := true
	off := false

	tests := []struct {
		name     string
		fw       FileWriter
		wantMode os.FileMode
	}{
		{
			name: "default mode no roll",
			fw: FileWriter{
				Roll: &off,
			},
			wantMode: 0o600,
		},
		{
			name: "default mode roll",
			fw: FileWriter{
				Roll: &on,
			},
			wantMode: 0o600,
		},
		{
			name: "custom mode no roll",
			fw: FileWriter{
				Roll: &off,
				Mode: 0o666,
			},
			wantMode: 0o666,
		},
		{
			name: "custom mode roll",
			fw: FileWriter{
				Roll: &on,
				Mode: 0o666,
			},
			wantMode: 0o666,
		},
	}

	m := syscall.Umask(0o000)
	defer syscall.Umask(m)

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			dir, err := os.MkdirTemp("", "caddytest")
			if err != nil {
				t.Fatalf("failed to create tempdir: %v", err)
			}
			defer os.RemoveAll(dir)
			fpath := filepath.Join(dir, "test.log")
			tt.fw.Filename = fpath

			logger, err := tt.fw.OpenWriter()
			if err != nil {
				t.Fatalf("failed to create file: %v", err)
			}
			defer logger.Close()

			st, err := os.Stat(fpath)
			if err != nil {
				t.Fatalf("failed to check file permissions: %v", err)
			}

			if st.Mode() != tt.wantMode {
				t.Errorf("%s: file mode is %v, want %v", tt.name, st.Mode(), tt.wantMode)
			}
		})
	}
}

func TestFileRotationPreserveMode(t *testing.T) {
	m := syscall.Umask(0o000)
	defer syscall.Umask(m)

	dir, err := os.MkdirTemp("", "caddytest")
	if err != nil {
		t.Fatalf("failed to create tempdir: %v", err)
	}
	defer os.RemoveAll(dir)

	fpath := path.Join(dir, "test.log")

	roll := true
	mode := fileMode(0o640)
	fw := FileWriter{
		Filename:   fpath,
		Mode:       mode,
		Roll:       &roll,
		RollSizeMB: 1,
	}

	logger, err := fw.OpenWriter()
	if err != nil {
		t.Fatalf("failed to create file: %v", err)
	}
	defer logger.Close()

	b := make([]byte, 1024*1024-1000)
	logger.Write(b)
	logger.Write(b[0:2000])

	files, err := os.ReadDir(dir)
	if err != nil {
		t.Fatalf("failed to read temporary log dir: %v", err)
	}

	// We might get 2 or 3 files depending
	// on the race between compressed log file generation,
	// removal of the non compressed file and reading the directory.
	// Ordering of the files are [ test-*.log test-*.log.gz test.log ]
	if len(files) < 2 || len(files) > 3 {
		t.Log("got files: ", files)
		t.Fatalf("got %v files want 2", len(files))
	}

	wantPattern := "test-*-*-*-*-*.*.log"
	test_date_log := files[0]
	if m, _ := path.Match(wantPattern, test_date_log.Name()); m != true {
		t.Fatalf("got %v filename want %v", test_date_log.Name(), wantPattern)
	}

	st, err := os.Stat(path.Join(dir, test_date_log.Name()))
	if err != nil {
		t.Fatalf("failed to check file permissions: %v", err)
	}

	if st.Mode() != os.FileMode(mode) {
		t.Errorf("file mode is %v, want %v", st.Mode(), mode)
	}

	test_dot_log := files[len(files)-1]
	if test_dot_log.Name() != "test.log" {
		t.Fatalf("got %v filename want test.log", test_dot_log.Name())
	}

	st, err = os.Stat(path.Join(dir, test_dot_log.Name()))
	if err != nil {
		t.Fatalf("failed to check file permissions: %v", err)
	}

	if st.Mode() != os.FileMode(mode) {
		t.Errorf("file mode is %v, want %v", st.Mode(), mode)
	}
}

func TestFileRotationPreserveModeWithUmask(t *testing.T) {
	m := syscall.Umask(0o022)
	defer syscall.Umask(m)

	dir, err := os.MkdirTemp("", "caddytest")
	if err != nil {
		t.Fatalf("failed to create tempdir: %v", err)
	}
	defer os.RemoveAll(dir)

	fpath := path.Join(dir, "test.log")

	roll := true
	mode := fileMode(0o660)
	fw := FileWriter{
		Filename:   fpath,
		Mode:       mode,
		Roll:       &roll,
		RollSizeMB: 1,
	}

	logger, err := fw.OpenWriter()
	if err != nil {
		t.Fatalf("failed to create file: %v", err)
	}
	defer logger.Close()

	b := make([]byte, 1024*1024-1000)
	logger.Write(b)
	logger.Write(b[0:2000])

	st, err := os.Stat(fpath)
	if err != nil {
		t.Fatalf("failed to check file permissions: %v", err)
	}

	if got := st.Mode().Perm(); got != os.FileMode(mode) {
		t.Errorf("file mode after rotation is %v, want %v", got, mode)
	}
}

func TestFileModeConfig(t *testing.T) {
	tests := []struct {
		name    string
		d       *caddyfile.Dispenser
		fw      FileWriter
		wantErr bool
	}{
		{
			name: "set mode",
			d: caddyfile.NewTestDispenser(`
file test.log {
	mode 0666
}
`),
			fw: FileWriter{
				Mode: 0o666,
			},
			wantErr: false,
		},
		{
			name: "set mode 3 digits",
			d: caddyfile.NewTestDispenser(`
file test.log {
	mode 666
}
`),
			fw: FileWriter{
				Mode: 0o666,
			},
			wantErr: false,
		},
		{
			name: "set mode 2 digits",
			d: caddyfile.NewTestDispenser(`
file test.log {
	mode 66
}
`),
			fw: FileWriter{
				Mode: 0o066,
			},
			wantErr: false,
		},
		{
			name: "set mode 1 digits",
			d: caddyfile.NewTestDispenser(`
file test.log {
	mode 6
}
`),
			fw: FileWriter{
				Mode: 0o006,
			},
			wantErr: false,
		},
		{
			name: "invalid mode",
			d: caddyfile.NewTestDispenser(`
file test.log {
	mode foobar
}
`),
			fw:      FileWriter{},
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			fw := &FileWriter{}
			if err := fw.UnmarshalCaddyfile(tt.d); (err != nil) != tt.wantErr {
				t.Fatalf("UnmarshalCaddyfile() error = %v, want %v", err, tt.wantErr)
			}
			if fw.Mode != tt.fw.Mode {
				t.Errorf("got mode %v, want %v", fw.Mode, tt.fw.Mode)
			}
		})
	}
}

func TestFileModeJSON(t *testing.T) {
	tests := []struct {
		name    string
		config  string
		fw      FileWriter
		wantErr bool
	}{
		{
			name: "set mode",
			config: `
{
	"mode": "0666"
}
`,
			fw: FileWriter{
				Mode: 0o666,
			},
			wantErr: false,
		},
		{
			name: "set mode invalid value",
			config: `
{
	"mode": "0x666"
}
`,
			fw:      FileWriter{},
			wantErr: true,
		},
		{
			name: "set mode invalid string",
			config: `
{
	"mode": 777
}
`,
			fw:      FileWriter{},
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			fw := &FileWriter{}
			if err := json.Unmarshal([]byte(tt.config), fw); (err != nil) != tt.wantErr {
				t.Fatalf("UnmarshalJSON() error = %v, want %v", err, tt.wantErr)
			}
			if fw.Mode != tt.fw.Mode {
				t.Errorf("got mode %v, want %v", fw.Mode, tt.fw.Mode)
			}
		})
	}
}

func TestFileModeToJSON(t *testing.T) {
	tests := []struct {
		name    string
		mode    fileMode
		want    string
		wantErr bool
	}{
		{
			name:    "none zero",
			mode:    0o644,
			want:    `"0644"`,
			wantErr: false,
		},
		{
			name:    "zero mode",
			mode:    0,
			want:    `"0000"`,
			wantErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var b []byte
			var err error

			if b, err = json.Marshal(&tt.mode); (err != nil) != tt.wantErr {
				t.Fatalf("MarshalJSON() error = %v, want %v", err, tt.wantErr)
			}

			got := string(b[:])

			if got != tt.want {
				t.Errorf("got mode %v, want %v", got, tt.want)
			}
		})
	}
}

func TestFileModeModification(t *testing.T) {
	m := syscall.Umask(0o000)
	defer syscall.Umask(m)

	dir, err := os.MkdirTemp("", "caddytest")
	if err != nil {
		t.Fatalf("failed to create tempdir: %v", err)
	}
	defer os.RemoveAll(dir)

	fpath := path.Join(dir, "test.log")
	f_tmp, err := os.OpenFile(fpath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(0o600))
	if err != nil {
		t.Fatalf("failed to create test file: %v", err)
	}
	f_tmp.Close()

	fw := FileWriter{
		Mode:     0o666,
		Filename: fpath,
	}

	logger, err := fw.OpenWriter()
	if err != nil {
		t.Fatalf("failed to create file: %v", err)
	}
	defer logger.Close()

	st, err := os.Stat(fpath)
	if err != nil {
		t.Fatalf("failed to check file permissions: %v", err)
	}

	want := os.FileMode(fw.Mode)
	if st.Mode() != want {
		t.Errorf("file mode is %v, want %v", st.Mode(), want)
	}
}

func TestDirMode_Inherit(t *testing.T) {
	m := syscall.Umask(0)
	defer syscall.Umask(m)

	parent := t.TempDir()
	if err := os.Chmod(parent, 0o755); err != nil {
		t.Fatal(err)
	}

	targetDir := filepath.Join(parent, "a", "b")
	fw := &FileWriter{
		Filename: filepath.Join(targetDir, "test.log"),
		DirMode:  "inherit",
		Mode:     0o640,
		Roll:     func() *bool { f := false; return &f }(),
	}
	w, err := fw.OpenWriter()
	if err != nil {
		t.Fatal(err)
	}
	_ = w.Close()

	st, err := os.Stat(targetDir)
	if err != nil {
		t.Fatal(err)
	}
	if got := st.Mode().Perm(); got != 0o755 {
		t.Fatalf("dir perm = %o, want 0755", got)
	}
}

func TestDirMode_FromFile(t *testing.T) {
	m := syscall.Umask(0)
	defer syscall.Umask(m)

	base := t.TempDir()

	dir1 := filepath.Join(base, "logs1")
	fw1 := &FileWriter{
		Filename: filepath.Join(dir1, "app.log"),
		DirMode:  "from_file",
		Mode:     0o644, // => dir 0755
		Roll:     func() *bool { f := false; return &f }(),
	}
	w1, err := fw1.OpenWriter()
	if err != nil {
		t.Fatal(err)
	}
	_ = w1.Close()

	st1, err := os.Stat(dir1)
	if err != nil {
		t.Fatal(err)
	}
	if got := st1.Mode().Perm(); got != 0o755 {
		t.Fatalf("dir perm = %o, want 0755", got)
	}

	dir2 := filepath.Join(base, "logs2")
	fw2 := &FileWriter{
		Filename: filepath.Join(dir2, "app.log"),
		DirMode:  "from_file",
		Mode:     0o600, // => dir 0700
		Roll:     func() *bool { f := false; return &f }(),
	}
	w2, err := fw2.OpenWriter()
	if err != nil {
		t.Fatal(err)
	}
	_ = w2.Close()

	st2, err := os.Stat(dir2)
	if err != nil {
		t.Fatal(err)
	}
	if got := st2.Mode().Perm(); got != 0o700 {
		t.Fatalf("dir perm = %o, want 0700", got)
	}
}

func TestDirMode_ExplicitOctal(t *testing.T) {
	m := syscall.Umask(0)
	defer syscall.Umask(m)

	base := t.TempDir()
	dest := filepath.Join(base, "logs3")
	fw := &FileWriter{
		Filename: filepath.Join(dest, "app.log"),
		DirMode:  "0750",
		Mode:     0o640,
		Roll:     func() *bool { f := false; return &f }(),
	}
	w, err := fw.OpenWriter()
	if err != nil {
		t.Fatal(err)
	}
	_ = w.Close()

	st, err := os.Stat(dest)
	if err != nil {
		t.Fatal(err)
	}
	if got := st.Mode().Perm(); got != 0o750 {
		t.Fatalf("dir perm = %o, want 0750", got)
	}
}

func TestDirMode_Default0700(t *testing.T) {
	m := syscall.Umask(0)
	defer syscall.Umask(m)

	base := t.TempDir()
	dest := filepath.Join(base, "logs4")
	fw := &FileWriter{
		Filename: filepath.Join(dest, "app.log"),
		Mode:     0o640,
		Roll:     func() *bool { f := false; return &f }(),
	}
	w, err := fw.OpenWriter()
	if err != nil {
		t.Fatal(err)
	}
	_ = w.Close()

	st, err := os.Stat(dest)
	if err != nil {
		t.Fatal(err)
	}
	if got := st.Mode().Perm(); got != 0o700 {
		t.Fatalf("dir perm = %o, want 0700", got)
	}
}

func TestDirMode_UmaskInteraction(t *testing.T) {
	_ = syscall.Umask(0o022) // typical umask; restore after
	defer syscall.Umask(0)

	base := t.TempDir()
	dest := filepath.Join(base, "logs5")
	fw := &FileWriter{
		Filename: filepath.Join(dest, "app.log"),
		DirMode:  "0755",
		Mode:     0o644,
		Roll:     func() *bool { f := false; return &f }(),
	}
	w, err := fw.OpenWriter()
	if err != nil {
		t.Fatal(err)
	}
	_ = w.Close()

	st, err := os.Stat(dest)
	if err != nil {
		t.Fatal(err)
	}
	// 0755 &^ 0022 still 0755 for dirs; this just sanity-checks we didn't get stricter unexpectedly
	if got := st.Mode().Perm(); got != 0o755 {
		t.Fatalf("dir perm = %o, want 0755 (considering umask)", got)
	}
}

func TestCaddyfile_DirMode_Inherit(t *testing.T) {
	d := caddyfile.NewTestDispenser(`
file /var/log/app.log {
    dir_mode inherit
    mode 0640
}`)
	var fw FileWriter
	if err := fw.UnmarshalCaddyfile(d); err != nil {
		t.Fatal(err)
	}
	if fw.DirMode != "inherit" {
		t.Fatalf("got %q", fw.DirMode)
	}
	if fw.Mode != 0o640 {
		t.Fatalf("mode = %o", fw.Mode)
	}
}

func TestCaddyfile_DirMode_FromFile(t *testing.T) {
	d := caddyfile.NewTestDispenser(`
file /var/log/app.log {
    dir_mode from_file
    mode 0600
}`)
	var fw FileWriter
	if err := fw.UnmarshalCaddyfile(d); err != nil {
		t.Fatal(err)
	}
	if fw.DirMode != "from_file" {
		t.Fatalf("got %q", fw.DirMode)
	}
	if fw.Mode != 0o600 {
		t.Fatalf("mode = %o", fw.Mode)
	}
}

func TestCaddyfile_DirMode_Octal(t *testing.T) {
	d := caddyfile.NewTestDispenser(`
file /var/log/app.log {
    dir_mode 0755
}`)
	var fw FileWriter
	if err := fw.UnmarshalCaddyfile(d); err != nil {
		t.Fatal(err)
	}
	if fw.DirMode != "0755" {
		t.Fatalf("got %q", fw.DirMode)
	}
}

func TestCaddyfile_DirMode_Invalid(t *testing.T) {
	d := caddyfile.NewTestDispenser(`
file /var/log/app.log {
    dir_mode nope
}`)
	var fw FileWriter
	if err := fw.UnmarshalCaddyfile(d); err == nil {
		t.Fatal("expected error for invalid dir_mode")
	}
}

func TestCaddyfile_RollInterval(t *testing.T) {
	tests := []struct {
		name     string
		input    string
		wantSecs float64
		wantErr  bool
	}{
		{
			name: "day unit",
			input: `file /var/log/app.log {
    roll_interval 1d
}`,
			wantSecs: 86400,
		},
		{
			name: "hours unit",
			input: `file /var/log/app.log {
    roll_interval 24h
}`,
			wantSecs: 86400,
		},
		{
			name: "minutes unit",
			input: `file /var/log/app.log {
    roll_interval 30m
}`,
			wantSecs: 1800,
		},
		{
			name: "fractional days",
			input: `file /var/log/app.log {
    roll_interval 1.5d
}`,
			wantSecs: 129600,
		},
		{
			name: "invalid duration",
			input: `file /var/log/app.log {
    roll_interval bogus
}`,
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			d := caddyfile.NewTestDispenser(tt.input)
			var fw FileWriter
			err := fw.UnmarshalCaddyfile(d)
			if tt.wantErr {
				if err == nil {
					t.Fatal("expected error but got none")
				}
				return
			}
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
			gotSecs := time.Duration(fw.RollInterval).Seconds()
			if gotSecs != tt.wantSecs {
				t.Errorf("got %v seconds, want %v", gotSecs, tt.wantSecs)
			}
		})
	}
}
