// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.

package resource

import (
	"context"
	_ "encoding/json"
	"fmt"
	"github.com/stretchr/testify/assert"
	"gopkg.in/yaml.v3"
	_ "io"
	_ "log"
	_ "net/http"
	_ "net/http/httptest"
	_ "net/url"
	"os"
	"path/filepath"
	_ "strings"
	"syscall"
	"testing"
	"time"
	"os/user"
)

func TestNewFileResource(t *testing.T) {
	f := NewFile()
	assert.NotEqual(t, nil, f)
}

func TestNewFileNormalized(t *testing.T) {
	file := fmt.Sprintf("%s/%s", TempDir, "bar/../fooread.txt")
	absFilePath,_ := filepath.Abs(file)
	f := NewNormalizedFile()
	assert.NotNil(t, f)
	assert.Nil(t, f.SetURI("file://" + file))

	assert.NotEqual(t, file, f.Path)
	assert.Equal(t, absFilePath, f.Path)

	assert.NotEqual(t, "file://" + file, f.URI())
	assert.Equal(t, "file://" + absFilePath, f.URI())
}

func TestApplyResourceTransformation(t *testing.T) {
	f := NewFile()
	assert.NotEqual(t, nil, f)

	//e := f.Apply()
	//assert.Equal(t, nil, e)
}

func TestReadFile(t *testing.T) {
	ctx := context.Background()
	file, _ := filepath.Abs(filepath.Join(TempDir, "fooread.txt"))

	declarationAttributes := `
  path: "%s"
  owner: "nobody"
  group: "nobody"
  mode: "0600"
  atime: 2001-12-15T01:01:01.000000001Z
  ctime: %s
  mtime: 2001-12-15T01:01:01.000000001Z
  content: |-
    test line 1
    test line 2
  sha256: f2082f984f1bf1a7886e2af32ccc9ca474fbff3553d131204b070c438114dd51
  size: 23
  filetype: "regular"
  state: present
`

	decl := fmt.Sprintf(declarationAttributes, file, "2001-12-15T01:01:01.000000001Z")

	testFile := NewFile()
	e := testFile.LoadDecl(decl)
	assert.Equal(t, nil, e)
	applyErr := testFile.Apply()
	assert.Nil(t, applyErr)

	f := NewFile()
	assert.NotEqual(t, nil, f)

	f.Path = file
	r, e := f.Read(ctx)
	assert.Equal(t, nil, e)
	assert.Equal(t, "nobody", f.Owner)

	info, statErr := os.Stat(file)
	assert.Nil(t, statErr)
	stat, ok := info.Sys().(*syscall.Stat_t)
	assert.True(t, ok)
	cTime := time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec))

	expected := fmt.Sprintf(declarationAttributes, file, cTime.Format(time.RFC3339Nano))
	assert.YAMLEq(t, expected, string(r))
}

func TestReadFileError(t *testing.T) {
	ctx := context.Background()
	file, _ := filepath.Abs(filepath.Join(TempDir, "missingfile.txt"))

	f := NewFile()
	assert.NotEqual(t, nil, f)
	f.Path = file
	_, e := f.Read(ctx)
	assert.True(t, os.IsNotExist(e))
	assert.Equal(t, "absent", f.State)
}

func TestCreateFile(t *testing.T) {
	file, _ := filepath.Abs(filepath.Join(TempDir, "foo.txt"))

	decl := fmt.Sprintf(`
  path: "%s"
  owner: "nobody"
  group: "nobody"
  mode: "0600"
  content: |-
    test line 1
    test line 2
  state: present
`, file)

	f := NewFile()
	e := f.LoadDecl(decl)
	assert.Equal(t, nil, e)
	assert.Equal(t, "nobody", f.Owner)

	applyErr := f.Apply()
	assert.Equal(t, nil, applyErr)
	assert.FileExists(t, file, nil)
	s, e := os.Stat(file)
	assert.Equal(t, nil, e)

	assert.Greater(t, s.Size(), int64(0))

	f.State = "absent"
	assert.Equal(t, nil, f.Apply())
	assert.NoFileExists(t, file, nil)
}

func TestFileType(t *testing.T) {
	fileType := []byte(`
filetype: "directory"
`)
	var testFile File
	err := yaml.Unmarshal(fileType, &testFile)
	assert.Nil(t, err)

}

func TestFileDirectory(t *testing.T) {
	file, _ := filepath.Abs(filepath.Join(TempDir, "testdir"))

	decl := fmt.Sprintf(`
  path: "%s"
  owner: "nobody"
  group: "nobody"
  mode: "0700"
  filetype: "directory"
  state: present
`, file)

	f := NewFile()
	e := f.LoadDecl(decl)
	assert.Equal(t, nil, e)
	assert.Equal(t, "nobody", f.Owner)

	applyErr := f.Apply()
	assert.Equal(t, nil, applyErr)
	assert.DirExists(t, file)

	f.State = "absent"
	deleteErr := f.Apply()
	assert.Nil(t, deleteErr)
	assert.NoDirExists(t, file)
}

func TestFileTimes(t *testing.T) {
	file, _ := filepath.Abs(filepath.Join(TempDir, "testtimes.txt"))
	decl := fmt.Sprintf(`
  path: "%s"
  owner: "nobody"
  group: "nobody"
  mtime: 2001-12-15T01:01:01.1Z
  mode: "0600"
  filtetype: "regular"
  state: "present"
`, file)

	expectedTime, timeErr := time.Parse(time.RFC3339, "2001-12-15T01:01:01.1Z")
	assert.Nil(t, timeErr)

	f := NewFile()
	e := f.LoadDecl(decl)
	assert.Nil(t, e)
	assert.Equal(t, "nobody", f.Owner)
	assert.True(t, f.Mtime.Equal(expectedTime))
}

func TestFileSetURI(t *testing.T) {
	file, _ := filepath.Abs(filepath.Join(TempDir, "testuri.txt"))
	f := NewFile()
	assert.NotNil(t, f)
	e := f.SetURI("file://" + file)
	assert.Nil(t, e)
	assert.Equal(t, "file", f.Type())
	assert.Equal(t, file, f.Path)
}

func TestFileNormalizePath(t *testing.T) {
	absFile, absFilePathErr := filepath.Abs(filepath.Join(TempDir, "./testuri.txt"))
	assert.Nil(t, absFilePathErr)

	file := filepath.Join(TempDir, "./testuri.txt")

	f := NewFile()
	assert.NotNil(t, f)

	f.Path = file
	e := f.NormalizePath()
	assert.Nil(t, e)

	assert.Equal(t, absFile, f.Path)
}

func TestFileUpdateAttributesFromFileInfo(t *testing.T) {
	f := NewFile()
	assert.NotNil(t, f)

	info, e := os.Lstat(TempDir)
	assert.Nil(t, e)

	updateAttributesErr := f.UpdateAttributesFromFileInfo(info)
	assert.Nil(t, updateAttributesErr)
	assert.Equal(t, DirectoryFile, f.FileType)
}

func TestFileReadStat(t *testing.T) {
	ctx := context.Background()
	link := filepath.Join(TempDir, "link.txt")
	linkTargetFile := filepath.Join(TempDir, "testuri.txt")

	f := NewFile()
	assert.NotNil(t, f)

	f.Path = linkTargetFile
	e := f.NormalizePath()
	assert.Nil(t, e)

	statErr := f.ReadStat()
	assert.Error(t, statErr)

	f.Owner = "nobody"
	f.Group = "nobody"
	f.State = "present"
	assert.Nil(t, f.Apply())

	assert.Nil(t, f.ReadStat())

	l := NewFile()
	assert.NotNil(t, l)

	l.FileType = SymbolicLinkFile
	l.Path = link
	l.Target = linkTargetFile
	l.State = "present"

	applyErr := l.Apply()
	assert.Nil(t, applyErr)
	readStatErr := l.ReadStat()
	assert.Nil(t, readStatErr)

	testRead := NewFile()
	testRead.Path = link
	_,testReadErr := testRead.Read(ctx)
	assert.Nil(t, testReadErr)
	assert.Equal(t, linkTargetFile, testRead.Target)
}

func TestFileResourceFileInfo(t *testing.T) {
	testFile := filepath.Join(TempDir, "testuri.txt")

	f := NewFile()
	assert.NotNil(t, f)

	f.Path = testFile
	f.Mode = "0600"
	f.State = "present"
	assert.Nil(t, f.Apply())

	_, readErr := f.Read(context.Background())
	assert.Nil(t, readErr)

	fi := f.FileInfo()
	assert.Equal(t, os.FileMode(0600), fi.Mode().Perm())
}

func TestFileClone(t *testing.T) {
	ctx := context.Background()
	testFile := filepath.Join(TempDir, "testorig.txt")
	testCloneFile := filepath.Join(TempDir, "testclone.txt")

	f := NewFile()
	assert.NotNil(t, f)

	f.Path = testFile
	f.Mode = "0600"
	f.State = "present"
	assert.Nil(t, f.Apply())
	
	_,readErr := f.Read(ctx)
	assert.Nil(t, readErr)

	time.Sleep(100 * time.Millisecond)

	clone := f.Clone().(*File)
	assert.Equal(t, f, clone)
	clone.Mtime = time.Time{}
	clone.Path = testCloneFile
	assert.Nil(t, clone.Apply())

	_,updateReadErr := f.Read(ctx)
	assert.Nil(t, updateReadErr)

	_, cloneReadErr := clone.Read(ctx)
	assert.Nil(t, cloneReadErr)

	fmt.Printf("file %#v\nclone %#v\n", f, clone) 
	assert.NotEqual(t, f.Mtime, clone.Mtime)
}

func TestFileErrors(t *testing.T) {
	ctx := context.Background()
	testFile := filepath.Join(TempDir, "testerr.txt")

	f := NewFile()
	assert.NotNil(t, f)

	f.Path = testFile
	f.Mode = "631"
	f.State = "present"
	assert.Nil(t, f.Apply())
	
	read := NewFile()
	read.Path = testFile
	_, readErr := read.Read(ctx)
	assert.Nil(t, readErr)
	assert.Equal(t, "0631", read.Mode)

	f.Mode = "900"
	assert.ErrorAs(t, f.Apply(), &ErrInvalidFileMode, "Apply should fail with NumError when converting invalid octal")
	
	_, updateReadErr := read.Read(ctx)
	assert.Nil(t, updateReadErr)
	assert.Equal(t, "0631", read.Mode)

	f.Mode = "0631"
	f.Owner = "bar"
	uidErr := f.Apply()
	var UnknownUser user.UnknownUserError
	assert.Error(t, uidErr, UnknownUser)
	
}
