package handlers

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"syscall"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"

	"github.com/Wikid82/charon/backend/internal/config"
	"github.com/Wikid82/charon/backend/internal/models"
	"github.com/Wikid82/charon/backend/internal/services"
	"github.com/Wikid82/charon/backend/internal/util"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type stubPermissionChecker struct{}

type fakeNoStatFileInfo struct{}

func (fakeNoStatFileInfo) Name() string       { return "fake" }
func (fakeNoStatFileInfo) Size() int64        { return 0 }
func (fakeNoStatFileInfo) Mode() os.FileMode  { return 0 }
func (fakeNoStatFileInfo) ModTime() time.Time { return time.Time{} }
func (fakeNoStatFileInfo) IsDir() bool        { return false }
func (fakeNoStatFileInfo) Sys() any           { return nil }

func (stubPermissionChecker) Check(path, required string) util.PermissionCheck {
	return util.PermissionCheck{
		Path:     path,
		Required: required,
		Exists:   true,
		Writable: true,
		OwnerUID: 1000,
		OwnerGID: 1000,
		Mode:     "0755",
	}
}

func TestSystemPermissionsHandler_GetPermissions_Admin(t *testing.T) {

	cfg := config.Config{
		DatabasePath:   "/app/data/charon.db",
		ConfigRoot:     "/config",
		CaddyLogDir:    "/var/log/caddy",
		CrowdSecLogDir: "/var/log/crowdsec",
		PluginsDir:     "/app/plugins",
	}

	h := NewSystemPermissionsHandler(cfg, nil, stubPermissionChecker{})

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Set("role", "admin")
	c.Request = httptest.NewRequest(http.MethodGet, "/system/permissions", http.NoBody)

	h.GetPermissions(c)

	require.Equal(t, http.StatusOK, w.Code)

	var payload struct {
		Paths []map[string]any `json:"paths"`
	}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &payload))
	require.NotEmpty(t, payload.Paths)

	first := payload.Paths[0]
	require.NotEmpty(t, first["path"])
	require.NotEmpty(t, first["required"])
	require.NotEmpty(t, first["mode"])
}

func TestSystemPermissionsHandler_GetPermissions_NonAdmin(t *testing.T) {

	cfg := config.Config{}
	h := NewSystemPermissionsHandler(cfg, nil, stubPermissionChecker{})

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Set("role", "user")
	c.Request = httptest.NewRequest(http.MethodGet, "/system/permissions", http.NoBody)

	h.GetPermissions(c)

	require.Equal(t, http.StatusForbidden, w.Code)

	var payload map[string]string
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &payload))
	require.Equal(t, "permissions_admin_only", payload["error_code"])
}

func TestSystemPermissionsHandler_RepairPermissions_NonRoot(t *testing.T) {
	if os.Geteuid() == 0 {
		t.Skip("test requires non-root execution")
	}


	cfg := config.Config{SingleContainer: true}
	h := NewSystemPermissionsHandler(cfg, nil, stubPermissionChecker{})

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Set("role", "admin")
	c.Request = httptest.NewRequest(http.MethodPost, "/system/permissions/repair", http.NoBody)

	h.RepairPermissions(c)

	require.Equal(t, http.StatusForbidden, w.Code)

	var payload map[string]string
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &payload))
	require.Equal(t, "permissions_non_root", payload["error_code"])
}

func TestSystemPermissionsHandler_HelperFunctions(t *testing.T) {
	t.Run("normalizePath", func(t *testing.T) {
		clean, code := normalizePath("/tmp/example")
		require.Equal(t, "/tmp/example", clean)
		require.Empty(t, code)

		clean, code = normalizePath("")
		require.Empty(t, clean)
		require.Equal(t, "permissions_invalid_path", code)

		clean, code = normalizePath("relative/path")
		require.Empty(t, clean)
		require.Equal(t, "permissions_invalid_path", code)
	})

	t.Run("containsParentReference", func(t *testing.T) {
		require.True(t, containsParentReference(".."))
		require.True(t, containsParentReference("../secrets"))
		require.True(t, containsParentReference("/var/../etc"))
		require.True(t, containsParentReference("/var/log/.."))
		require.False(t, containsParentReference("/var/log/charon"))
	})

	t.Run("isWithinAllowlist", func(t *testing.T) {
		allowlist := []string{"/app/data", "/config"}
		require.True(t, isWithinAllowlist("/app/data/charon.db", allowlist))
		require.True(t, isWithinAllowlist("/config/caddy", allowlist))
		require.False(t, isWithinAllowlist("/etc/passwd", allowlist))
	})

	t.Run("targetMode", func(t *testing.T) {
		require.Equal(t, "0700", targetMode(true, false))
		require.Equal(t, "0770", targetMode(true, true))
		require.Equal(t, "0600", targetMode(false, false))
		require.Equal(t, "0660", targetMode(false, true))
	})

	t.Run("parseMode", func(t *testing.T) {
		mode, err := parseMode("0640")
		require.NoError(t, err)
		require.Equal(t, os.FileMode(0640), mode)

		_, err = parseMode("")
		require.Error(t, err)

		_, err = parseMode("invalid")
		require.Error(t, err)
	})

	t.Run("mapRepairErrorCode", func(t *testing.T) {
		require.Equal(t, "", mapRepairErrorCode(nil))
		require.Equal(t, "permissions_readonly", mapRepairErrorCode(syscall.EROFS))
		require.Equal(t, "permissions_write_denied", mapRepairErrorCode(syscall.EACCES))
		require.Equal(t, "permissions_repair_failed", mapRepairErrorCode(syscall.EINVAL))
	})
}

func TestSystemPermissionsHandler_PathHasSymlink(t *testing.T) {
	root := t.TempDir()

	realDir := filepath.Join(root, "real")
	require.NoError(t, os.Mkdir(realDir, 0o750))

	plainPath := filepath.Join(realDir, "file.txt")
	require.NoError(t, os.WriteFile(plainPath, []byte("ok"), 0o600))

	hasSymlink, err := pathHasSymlink(plainPath)
	require.NoError(t, err)
	require.False(t, hasSymlink)

	linkDir := filepath.Join(root, "link")
	require.NoError(t, os.Symlink(realDir, linkDir))

	symlinkedPath := filepath.Join(linkDir, "file.txt")
	hasSymlink, err = pathHasSymlink(symlinkedPath)
	require.NoError(t, err)
	require.True(t, hasSymlink)

	_, err = pathHasSymlink(filepath.Join(root, "missing", "file.txt"))
	require.Error(t, err)
}

func TestSystemPermissionsHandler_NewDefaultsCheckerToOSChecker(t *testing.T) {
	h := NewSystemPermissionsHandler(config.Config{}, nil, nil)
	require.NotNil(t, h)
	require.NotNil(t, h.checker)
}

func TestSystemPermissionsHandler_RepairPermissions_DisabledWhenNotSingleContainer(t *testing.T) {

	h := NewSystemPermissionsHandler(config.Config{SingleContainer: false}, nil, stubPermissionChecker{})

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Set("role", "admin")
	c.Request = httptest.NewRequest(http.MethodPost, "/system/permissions/repair", bytes.NewBufferString(`{"paths":["/tmp"]}`))
	c.Request.Header.Set("Content-Type", "application/json")

	h.RepairPermissions(c)

	require.Equal(t, http.StatusForbidden, w.Code)
	var payload map[string]string
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &payload))
	require.Equal(t, "permissions_repair_disabled", payload["error_code"])
}

func TestSystemPermissionsHandler_RepairPermissions_InvalidJSON(t *testing.T) {
	if os.Geteuid() != 0 {
		t.Skip("test requires root execution")
	}


	root := t.TempDir()
	dataDir := filepath.Join(root, "data")
	require.NoError(t, os.MkdirAll(dataDir, 0o750))

	cfg := config.Config{
		SingleContainer: true,
		DatabasePath:    filepath.Join(dataDir, "charon.db"),
		ConfigRoot:      dataDir,
		CaddyLogDir:     dataDir,
		CrowdSecLogDir:  dataDir,
		PluginsDir:      filepath.Join(root, "plugins"),
	}

	h := NewSystemPermissionsHandler(cfg, nil, stubPermissionChecker{})

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Set("role", "admin")
	c.Request = httptest.NewRequest(http.MethodPost, "/system/permissions/repair", bytes.NewBufferString(`{"paths":`))
	c.Request.Header.Set("Content-Type", "application/json")

	h.RepairPermissions(c)

	require.Equal(t, http.StatusBadRequest, w.Code)
}

func TestSystemPermissionsHandler_RepairPermissions_Success(t *testing.T) {
	if os.Geteuid() != 0 {
		t.Skip("test requires root execution")
	}


	root := t.TempDir()
	dataDir := filepath.Join(root, "data")
	require.NoError(t, os.MkdirAll(dataDir, 0o750))

	targetFile := filepath.Join(dataDir, "repair-target.txt")
	require.NoError(t, os.WriteFile(targetFile, []byte("repair"), 0o600))

	cfg := config.Config{
		SingleContainer: true,
		DatabasePath:    filepath.Join(dataDir, "charon.db"),
		ConfigRoot:      dataDir,
		CaddyLogDir:     dataDir,
		CrowdSecLogDir:  dataDir,
		PluginsDir:      filepath.Join(root, "plugins"),
	}

	h := NewSystemPermissionsHandler(cfg, nil, stubPermissionChecker{})

	body := fmt.Sprintf(`{"paths":[%q],"group_mode":false}`, targetFile)
	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Set("role", "admin")
	c.Request = httptest.NewRequest(http.MethodPost, "/system/permissions/repair", bytes.NewBufferString(body))
	c.Request.Header.Set("Content-Type", "application/json")

	h.RepairPermissions(c)

	require.Equal(t, http.StatusOK, w.Code)

	var payload struct {
		Paths []permissionsRepairResult `json:"paths"`
	}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &payload))
	require.Len(t, payload.Paths, 1)
	require.Equal(t, targetFile, payload.Paths[0].Path)
	require.NotEqual(t, "error", payload.Paths[0].Status)
}

func TestSystemPermissionsHandler_RepairPermissions_NonAdmin(t *testing.T) {

	h := NewSystemPermissionsHandler(config.Config{SingleContainer: true}, nil, stubPermissionChecker{})

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Set("role", "user")
	c.Request = httptest.NewRequest(http.MethodPost, "/system/permissions/repair", bytes.NewBufferString(`{"paths":["/tmp"]}`))
	c.Request.Header.Set("Content-Type", "application/json")

	h.RepairPermissions(c)

	require.Equal(t, http.StatusForbidden, w.Code)
}

func TestSystemPermissionsHandler_RepairPermissions_InvalidJSONWhenRoot(t *testing.T) {
	if os.Geteuid() != 0 {
		t.Skip("test requires root execution")
	}

	root := t.TempDir()
	dataDir := filepath.Join(root, "data")
	require.NoError(t, os.MkdirAll(dataDir, 0o750))

	h := NewSystemPermissionsHandler(config.Config{
		SingleContainer: true,
		DatabasePath:    filepath.Join(dataDir, "charon.db"),
		ConfigRoot:      dataDir,
		CaddyLogDir:     dataDir,
		CrowdSecLogDir:  dataDir,
	}, nil, stubPermissionChecker{})

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Set("role", "admin")
	c.Request = httptest.NewRequest(http.MethodPost, "/system/permissions/repair", bytes.NewBufferString(`{"paths":`))
	c.Request.Header.Set("Content-Type", "application/json")

	h.RepairPermissions(c)

	require.Equal(t, http.StatusBadRequest, w.Code)
}

func TestSystemPermissionsHandler_DefaultPathsAndAllowlistRoots(t *testing.T) {
	h := NewSystemPermissionsHandler(config.Config{
		DatabasePath:   "/app/data/charon.db",
		ConfigRoot:     "/app/config",
		CaddyLogDir:    "/var/log/caddy",
		CrowdSecLogDir: "/var/log/crowdsec",
		PluginsDir:     "/app/plugins",
	}, nil, stubPermissionChecker{})

	paths := h.defaultPaths()
	require.Len(t, paths, 11)
	require.Equal(t, "/app/data", paths[0].Path)
	require.Equal(t, "/app/plugins", paths[len(paths)-1].Path)

	roots := h.allowlistRoots()
	require.Equal(t, []string{"/app/data", "/app/config", "/var/log/caddy", "/var/log/crowdsec"}, roots)
}

func TestSystemPermissionsHandler_IsOwnedByFalseWhenSysNotStat(t *testing.T) {
	owned := isOwnedBy(fakeNoStatFileInfo{}, os.Geteuid(), os.Getegid())
	require.False(t, owned)
}

func TestSystemPermissionsHandler_IsWithinAllowlist_RelErrorBranch(t *testing.T) {
	tmp := t.TempDir()
	inAllow := filepath.Join(tmp, "a", "b")
	require.NoError(t, os.MkdirAll(inAllow, 0o750))

	badRoot := string([]byte{'/', 0, 'x'})
	allowed := isWithinAllowlist(inAllow, []string{badRoot, tmp})
	require.True(t, allowed)
}

func TestSystemPermissionsHandler_IsWithinAllowlist_AllRelErrorsReturnFalse(t *testing.T) {
	badRoot1 := string([]byte{'/', 0, 'x'})
	badRoot2 := string([]byte{'/', 0, 'y'})
	allowed := isWithinAllowlist("/tmp/some/path", []string{badRoot1, badRoot2})
	require.False(t, allowed)
}

func TestSystemPermissionsHandler_LogAudit_PersistsAuditWithUserID(t *testing.T) {

	db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&models.SecurityAudit{}))

	securitySvc := services.NewSecurityService(db)
	h := NewSystemPermissionsHandler(config.Config{}, securitySvc, stubPermissionChecker{})

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Set("role", "admin")
	c.Set("userID", 42)
	c.Request = httptest.NewRequest(http.MethodGet, "/system/permissions", http.NoBody)

	require.NotPanics(t, func() {
		h.logAudit(c, "permissions_diagnostics", "ok", "", 2)
	})
}

func TestSystemPermissionsHandler_LogAudit_PersistsAuditWithUnknownActor(t *testing.T) {

	db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&models.SecurityAudit{}))

	securitySvc := services.NewSecurityService(db)
	h := NewSystemPermissionsHandler(config.Config{}, securitySvc, stubPermissionChecker{})

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Set("role", "admin")
	c.Request = httptest.NewRequest(http.MethodGet, "/system/permissions", http.NoBody)

	require.NotPanics(t, func() {
		h.logAudit(c, "permissions_diagnostics", "ok", "", 1)
	})
}

func TestSystemPermissionsHandler_RepairPath_Branches(t *testing.T) {
	h := NewSystemPermissionsHandler(config.Config{}, nil, stubPermissionChecker{})
	allowRoot := t.TempDir()
	allowlist := []string{allowRoot}

	t.Run("invalid path", func(t *testing.T) {
		result := h.repairPath("", false, allowlist)
		require.Equal(t, "error", result.Status)
		require.Equal(t, "permissions_invalid_path", result.ErrorCode)
	})

	t.Run("missing path", func(t *testing.T) {
		missingPath := filepath.Join(allowRoot, "missing-file.txt")
		result := h.repairPath(missingPath, false, allowlist)
		require.Equal(t, "error", result.Status)
		require.Equal(t, "permissions_missing_path", result.ErrorCode)
	})

	t.Run("symlink leaf rejected", func(t *testing.T) {
		target := filepath.Join(allowRoot, "target.txt")
		require.NoError(t, os.WriteFile(target, []byte("ok"), 0o600))
		link := filepath.Join(allowRoot, "link.txt")
		require.NoError(t, os.Symlink(target, link))

		result := h.repairPath(link, false, allowlist)
		require.Equal(t, "error", result.Status)
		require.Equal(t, "permissions_symlink_rejected", result.ErrorCode)
	})

	t.Run("symlink component rejected", func(t *testing.T) {
		realDir := filepath.Join(allowRoot, "real")
		require.NoError(t, os.MkdirAll(realDir, 0o750))
		realFile := filepath.Join(realDir, "file.txt")
		require.NoError(t, os.WriteFile(realFile, []byte("ok"), 0o600))

		linkDir := filepath.Join(allowRoot, "linkdir")
		require.NoError(t, os.Symlink(realDir, linkDir))

		result := h.repairPath(filepath.Join(linkDir, "file.txt"), false, allowlist)
		require.Equal(t, "error", result.Status)
		require.Equal(t, "permissions_symlink_rejected", result.ErrorCode)
	})

	t.Run("outside allowlist rejected", func(t *testing.T) {
		outsideFile := filepath.Join(t.TempDir(), "outside.txt")
		require.NoError(t, os.WriteFile(outsideFile, []byte("x"), 0o600))

		result := h.repairPath(outsideFile, false, allowlist)
		require.Equal(t, "error", result.Status)
		require.Equal(t, "permissions_outside_allowlist", result.ErrorCode)
	})

	t.Run("outside allowlist rejected before stat for missing path", func(t *testing.T) {
		outsideMissing := filepath.Join(t.TempDir(), "missing.txt")

		result := h.repairPath(outsideMissing, false, allowlist)
		require.Equal(t, "error", result.Status)
		require.Equal(t, "permissions_outside_allowlist", result.ErrorCode)
	})

	t.Run("unsupported type rejected", func(t *testing.T) {
		fifoPath := filepath.Join(allowRoot, "fifo")
		require.NoError(t, syscall.Mkfifo(fifoPath, 0o600))

		result := h.repairPath(fifoPath, false, allowlist)
		require.Equal(t, "error", result.Status)
		require.Equal(t, "permissions_unsupported_type", result.ErrorCode)
	})

	t.Run("already correct skipped", func(t *testing.T) {
		filePath := filepath.Join(allowRoot, "already-correct.txt")
		require.NoError(t, os.WriteFile(filePath, []byte("ok"), 0o600))

		result := h.repairPath(filePath, false, allowlist)
		require.Equal(t, "skipped", result.Status)
		require.Equal(t, "permissions_repair_skipped", result.ErrorCode)
		require.Equal(t, "0600", result.ModeAfter)
	})
}

func TestSystemPermissionsHandler_OSChecker_Check(t *testing.T) {
	if os.Geteuid() != 0 {
		t.Skip("test expects root-owned temp paths in CI")
	}

	tmp := t.TempDir()
	filePath := filepath.Join(tmp, "check.txt")
	require.NoError(t, os.WriteFile(filePath, []byte("ok"), 0o600))

	checker := OSChecker{}
	result := checker.Check(filePath, "rw")
	require.Equal(t, filePath, result.Path)
	require.Equal(t, "rw", result.Required)
	require.True(t, result.Exists)
}

func TestSystemPermissionsHandler_RepairPermissions_InvalidRequestBody_Root(t *testing.T) {
	if os.Geteuid() != 0 {
		t.Skip("test requires root execution")
	}


	tmp := t.TempDir()
	dataDir := filepath.Join(tmp, "data")
	require.NoError(t, os.MkdirAll(dataDir, 0o750))

	h := NewSystemPermissionsHandler(config.Config{
		SingleContainer: true,
		DatabasePath:    filepath.Join(dataDir, "charon.db"),
		ConfigRoot:      dataDir,
		CaddyLogDir:     dataDir,
		CrowdSecLogDir:  dataDir,
		PluginsDir:      filepath.Join(tmp, "plugins"),
	}, nil, stubPermissionChecker{})

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Set("role", "admin")
	c.Request = httptest.NewRequest(http.MethodPost, "/system/permissions/repair", bytes.NewBufferString(`{"group_mode":true}`))
	c.Request.Header.Set("Content-Type", "application/json")

	h.RepairPermissions(c)
	require.Equal(t, http.StatusBadRequest, w.Code)
}

func TestSystemPermissionsHandler_RepairPath_LstatInvalidArgument(t *testing.T) {
	h := NewSystemPermissionsHandler(config.Config{}, nil, stubPermissionChecker{})
	allowRoot := t.TempDir()

	result := h.repairPath("/tmp/\x00invalid", false, []string{allowRoot})
	require.Equal(t, "error", result.Status)
	require.Equal(t, "permissions_outside_allowlist", result.ErrorCode)
}

func TestSystemPermissionsHandler_RepairPath_RepairedBranch(t *testing.T) {
	if os.Geteuid() != 0 {
		t.Skip("test requires root execution")
	}

	h := NewSystemPermissionsHandler(config.Config{}, nil, stubPermissionChecker{})
	allowRoot := t.TempDir()
	targetFile := filepath.Join(allowRoot, "needs-repair.txt")
	require.NoError(t, os.WriteFile(targetFile, []byte("ok"), 0o600))

	result := h.repairPath(targetFile, true, []string{allowRoot})
	require.Equal(t, "repaired", result.Status)
	require.Equal(t, "0660", result.ModeAfter)

	info, err := os.Stat(targetFile)
	require.NoError(t, err)
	require.Equal(t, os.FileMode(0o660), info.Mode().Perm())
}

func TestSystemPermissionsHandler_NormalizePath_ParentRefBranches(t *testing.T) {
	clean, code := normalizePath("/../etc")
	require.Equal(t, "/etc", clean)
	require.Empty(t, code)

	clean, code = normalizePath("/var/../etc")
	require.Equal(t, "/etc", clean)
	require.Empty(t, code)
}

func TestSystemPermissionsHandler_NormalizeAllowlist(t *testing.T) {
	allowlist := normalizeAllowlist([]string{"", "/tmp/data/..", "/var/log/charon"})
	require.Equal(t, []string{"/tmp", "/var/log/charon"}, allowlist)
}
