// Copyright 2025 Emiliano Spinella (eminwux)
//
// 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.
//
// SPDX-License-Identifier: Apache-2.0

package e2e_test

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/eminwux/kukeon/internal/consts"
	"github.com/eminwux/kukeon/internal/util/fs"
	v1beta1 "github.com/eminwux/kukeon/pkg/api/model/v1beta1"
)

// generateUniqueRealmName generates a unique realm name for tests.
func generateUniqueRealmName(t *testing.T) string {
	t.Helper()
	return fmt.Sprintf("e-r-%s", uniqueNameID())
}

// verifyRealmMetadataExists verifies realm metadata file exists.
func verifyRealmMetadataExists(t *testing.T, runPath, realmName string) bool {
	t.Helper()

	metadataPath := fs.RealmMetadataPath(runPath, realmName)
	_, err := os.Stat(metadataPath)
	return err == nil
}

// parseRealmListJSON parses kuke get realm --output json output.
func parseRealmListJSON(t *testing.T, output []byte) ([]v1beta1.RealmDoc, error) {
	t.Helper()

	var realms []v1beta1.RealmDoc
	if err := json.Unmarshal(output, &realms); err != nil {
		return nil, fmt.Errorf("failed to parse realm list JSON: %w", err)
	}

	return realms, nil
}

// parseRealmJSON parses kuke get realm <name> --output json output.
func parseRealmJSON(t *testing.T, output []byte) (*v1beta1.RealmDoc, error) {
	t.Helper()

	var realm v1beta1.RealmDoc
	if err := json.Unmarshal(output, &realm); err != nil {
		return nil, fmt.Errorf("failed to parse realm JSON: %w", err)
	}

	return &realm, nil
}

// verifyRealmInList verifies realm appears in kuke get realm list.
func verifyRealmInList(t *testing.T, host, realmName string) bool {
	t.Helper()

	args := append(buildKukeDaemonArgs(host), "get", "realm", "--output", "json")
	output := runReturningBinary(t, nil, kuke, args...)

	realms, err := parseRealmListJSON(t, output)
	if err != nil {
		t.Logf("failed to parse realm list: %v", err)
		return false
	}

	for _, realm := range realms {
		if realm.Metadata.Name == realmName {
			return true
		}
	}

	return false
}

// verifyRealmExists verifies realm can be retrieved individually.
func verifyRealmExists(t *testing.T, host, realmName string) bool {
	t.Helper()

	args := append(buildKukeDaemonArgs(host), "get", "realm", realmName, "--output", "json")
	exitCode, stdout, _ := runBinary(t, nil, kuke, args...)

	if exitCode != 0 {
		return false
	}

	realm, err := parseRealmJSON(t, stdout)
	if err != nil {
		t.Logf("failed to parse realm JSON: %v", err)
		return false
	}

	return realm.Metadata.Name == realmName
}

// cleanupRealm deletes a realm with cascade through the per-test daemon. The
// caller must register this cleanup after startKukeondDaemon so t.Cleanup's
// LIFO order runs the delete before the daemon is signaled (otherwise the
// kuke client would dial a closed socket and the realm would leak).
func cleanupRealm(t *testing.T, host, realmName string) {
	t.Helper()

	args := append(buildKukeDaemonArgs(host), "delete", "realm", realmName, "--cascade")
	// Don't fail if realm doesn't exist
	_, _, _ = runBinary(t, nil, kuke, args...)
}

// TestKuke_NoRealms tests kuke get realm when no realms exist.
func TestKuke_NoRealms(t *testing.T) {
	t.Parallel()

	runPath := getRandomRunPath(t)
	host := startKukeondDaemon(t, runPath)

	args := append(buildKukeDaemonArgs(host), "get", "realm", "--output", "json")
	output := runReturningBinary(t, nil, kuke, args...)

	var realms []v1beta1.RealmDoc
	if err := json.Unmarshal(output, &realms); err != nil {
		t.Fatalf("failed to parse JSON: %v", err)
	}

	if len(realms) != 0 {
		t.Fatalf("expected empty realm list, got %d realms", len(realms))
	}
}

// TestKuke_Create_Realm_Help tests kuke create realm help.
func TestKuke_Create_Realm_Help(t *testing.T) {
	t.Parallel()

	exitCode, stdout, _ := runBinary(t, nil, kuke, "create", "realm", "-h")
	if exitCode != 0 {
		t.Fatalf("expected exit code 0, got %d", exitCode)
	}
	if len(stdout) == 0 {
		t.Fatal("expected non-empty output")
	}
}

// TestKuke_Get_Realm_Help tests kuke get realm help.
func TestKuke_Get_Realm_Help(t *testing.T) {
	t.Parallel()

	exitCode, stdout, _ := runBinary(t, nil, kuke, "get", "realm", "-h")
	if exitCode != 0 {
		t.Fatalf("expected exit code 0, got %d", exitCode)
	}
	if len(stdout) == 0 {
		t.Fatal("expected non-empty output")
	}
}

// TestKuke_CreateRealm_VerifyState tests realm creation with state-based verification.
func TestKuke_CreateRealm_VerifyState(t *testing.T) {
	t.Parallel()

	// Setup
	runPath := getRandomRunPath(t)
	host := startKukeondDaemon(t, runPath)
	realmName := generateUniqueRealmName(t)

	// Cleanup: Delete realm (routed through the same per-test daemon)
	t.Cleanup(func() {
		cleanupRealm(t, host, realmName)
	})

	// Step 1: Create realm
	args := append(buildKukeDaemonArgs(host), "create", "realm", realmName)
	runReturningBinary(t, nil, kuke, args...)

	// Step 2: Verify containerd namespace exists
	// Get namespace from realm (defaults to realm name)
	namespace := realmName
	if !verifyContainerdNamespace(t, namespace) {
		t.Fatalf("containerd namespace %q not found after realm creation", namespace)
	}

	// Step 3: Verify metadata file exists
	if !verifyRealmMetadataExists(t, runPath, realmName) {
		t.Fatalf("realm metadata file not found for realm %q", realmName)
	}

	// Step 4: Verify realm appears in list (JSON parsing)
	if !verifyRealmInList(t, host, realmName) {
		t.Fatalf("realm %q not found in realm list", realmName)
	}

	// Step 5: Verify realm can be retrieved individually
	if !verifyRealmExists(t, host, realmName) {
		t.Fatalf("realm %q cannot be retrieved individually", realmName)
	}

	// Step 6: Verify cgroup path exists
	// Get realm JSON to extract cgroup path
	args = append(buildKukeDaemonArgs(host), "get", "realm", realmName, "--output", "json")
	output := runReturningBinary(t, nil, kuke, args...)

	realm, err := parseRealmJSON(t, output)
	if err != nil {
		t.Fatalf("failed to parse realm JSON: %v", err)
	}

	if realm.Status.CgroupPath == "" {
		t.Fatal("realm cgroup path is empty")
	}

	// Verify cgroup path exists in filesystem
	if !verifyCgroupPathExists(t, realm.Status.CgroupPath) {
		// Build full filesystem path for error message
		relativePath := strings.TrimPrefix(realm.Status.CgroupPath, "/")
		fullPath := filepath.Join(consts.CgroupFilesystemPath, relativePath)
		t.Fatalf("cgroup path %q (full path: %q) does not exist in filesystem", realm.Status.CgroupPath, fullPath)
	}

	// Also verify expected cgroup path structure: <configured-root>/{realmName}.
	// The actual path may include the current process's cgroup hierarchy before
	// the kukeon path, so we check if the path ends with the expected pattern.
	// Read the configured cgroup root from the daemon's realm response rather
	// than from consts.KukeonCgroupRoot so the test passes against a non-default
	// ClientConfiguration / ServerConfiguration profile (e.g. /kukeon-dev).
	cgroupRoot, err := getCgroupRoot(t, host, realmName)
	if err != nil {
		t.Fatalf("getCgroupRoot: %v", err)
	}
	expectedCgroupPath := cgroupRoot + "/" + realmName
	if !strings.HasSuffix(realm.Status.CgroupPath, expectedCgroupPath) {
		t.Logf(
			"cgroup path %q does not end with expected pattern %q, but verifying it exists anyway",
			realm.Status.CgroupPath,
			expectedCgroupPath,
		)
	}

	// Cleanup runs automatically via t.Cleanup()
}

// TestKuke_DeleteRealm_VerifyState tests realm deletion with state-based verification.
func TestKuke_DeleteRealm_VerifyState(t *testing.T) {
	t.Parallel()

	// Setup
	runPath := getRandomRunPath(t)
	host := startKukeondDaemon(t, runPath)
	realmName := generateUniqueRealmName(t)

	// Cleanup: Safety net (realm should already be deleted, but ensure cleanup if test fails partway)
	t.Cleanup(func() {
		cleanupRealm(t, host, realmName)
	})

	// Step 1: Create realm (prerequisite for deletion test)
	args := append(buildKukeDaemonArgs(host), "create", "realm", realmName)
	runReturningBinary(t, nil, kuke, args...)

	// Step 2: Verify realm exists initially (establish baseline)
	namespace := realmName
	if !verifyContainerdNamespace(t, namespace) {
		t.Fatalf("containerd namespace %q not found after realm creation", namespace)
	}

	if !verifyRealmMetadataExists(t, runPath, realmName) {
		t.Fatalf("realm metadata file not found for realm %q", realmName)
	}

	if !verifyRealmInList(t, host, realmName) {
		t.Fatalf("realm %q not found in realm list", realmName)
	}

	if !verifyRealmExists(t, host, realmName) {
		t.Fatalf("realm %q cannot be retrieved individually", realmName)
	}

	// Get realm JSON to extract cgroup path for later verification
	args = append(buildKukeDaemonArgs(host), "get", "realm", realmName, "--output", "json")
	output := runReturningBinary(t, nil, kuke, args...)

	realm, err := parseRealmJSON(t, output)
	if err != nil {
		t.Fatalf("failed to parse realm JSON: %v", err)
	}

	if realm.Status.CgroupPath == "" {
		t.Fatal("realm cgroup path is empty")
	}

	cgroupPath := realm.Status.CgroupPath

	if !verifyCgroupPathExists(t, cgroupPath) {
		t.Fatalf("cgroup path %q does not exist in filesystem", cgroupPath)
	}

	// Step 3: Delete the realm
	args = append(buildKukeDaemonArgs(host), "delete", "realm", realmName)
	runReturningBinary(t, nil, kuke, args...)

	// Step 4: Verify metadata file does NOT exist
	if verifyRealmMetadataExists(t, runPath, realmName) {
		t.Fatalf("realm metadata file still exists for realm %q after deletion", realmName)
	}

	// Step 5: Verify containerd namespace does NOT exist
	if verifyContainerdNamespace(t, namespace) {
		t.Fatalf("containerd namespace %q still exists after realm deletion", namespace)
	}

	// Step 6: Verify cgroup path does NOT exist
	if verifyCgroupPathExists(t, cgroupPath) {
		// Build full filesystem path for error message
		relativePath := strings.TrimPrefix(cgroupPath, "/")
		fullPath := filepath.Join(consts.CgroupFilesystemPath, relativePath)
		t.Fatalf("cgroup path %q (full path: %q) still exists after realm deletion", cgroupPath, fullPath)
	}

	// Step 7: Verify realm does NOT appear in list
	if verifyRealmInList(t, host, realmName) {
		t.Fatalf("realm %q still appears in realm list after deletion", realmName)
	}

	// Step 8: Verify individual get FAILS (returns non-zero exit code)
	args = append(buildKukeDaemonArgs(host), "get", "realm", realmName, "--output", "json")
	exitCode, _, _ := runBinary(t, nil, kuke, args...)
	if exitCode == 0 {
		t.Fatalf("expected get realm to fail after deletion, but got exit code 0")
	}

	// Cleanup runs automatically via t.Cleanup()
}

// TestKuke_PurgeRealm_VerifyState tests realm purging with comprehensive cleanup verification.
func TestKuke_PurgeRealm_VerifyState(t *testing.T) {
	t.Parallel()

	// Setup
	runPath := getRandomRunPath(t)
	host := startKukeondDaemon(t, runPath)
	realmName := generateUniqueRealmName(t)

	// Cleanup: Safety net (realm should already be purged, but ensure cleanup if test fails partway)
	t.Cleanup(func() {
		cleanupRealm(t, host, realmName)
	})

	// Step 1: Create realm (prerequisite for purge test)
	args := append(buildKukeDaemonArgs(host), "create", "realm", realmName)
	runReturningBinary(t, nil, kuke, args...)

	// Step 2: Verify realm exists initially (establish baseline)
	namespace := realmName
	if !verifyContainerdNamespace(t, namespace) {
		t.Fatalf("containerd namespace %q not found after realm creation", namespace)
	}

	if !verifyRealmMetadataExists(t, runPath, realmName) {
		t.Fatalf("realm metadata file not found for realm %q", realmName)
	}

	if !verifyRealmInList(t, host, realmName) {
		t.Fatalf("realm %q not found in realm list", realmName)
	}

	if !verifyRealmExists(t, host, realmName) {
		t.Fatalf("realm %q cannot be retrieved individually", realmName)
	}

	// Get realm JSON to extract cgroup path for later verification
	args = append(buildKukeDaemonArgs(host), "get", "realm", realmName, "--output", "json")
	output := runReturningBinary(t, nil, kuke, args...)

	realm, err := parseRealmJSON(t, output)
	if err != nil {
		t.Fatalf("failed to parse realm JSON: %v", err)
	}

	if realm.Status.CgroupPath == "" {
		t.Fatal("realm cgroup path is empty")
	}

	cgroupPath := realm.Status.CgroupPath

	if !verifyCgroupPathExists(t, cgroupPath) {
		t.Fatalf("cgroup path %q does not exist in filesystem", cgroupPath)
	}

	// Step 3: Purge the realm (comprehensive cleanup) — purge stays
	// in-process per the #565 AC: it must run regardless of daemon state
	// so cleanup paths are not coupled to the per-test daemon's lifetime.
	args = append(buildKukeRunPathArgs(runPath), "purge", "realm", realmName)
	runReturningBinary(t, nil, kuke, args...)

	// Step 4: Verify metadata file does NOT exist
	if verifyRealmMetadataExists(t, runPath, realmName) {
		t.Fatalf("realm metadata file still exists for realm %q after purge", realmName)
	}

	// Step 5: Verify containerd namespace does NOT exist
	if verifyContainerdNamespace(t, namespace) {
		t.Fatalf("containerd namespace %q still exists after realm purge", namespace)
	}

	// Step 6: Verify cgroup path does NOT exist
	if verifyCgroupPathExists(t, cgroupPath) {
		// Build full filesystem path for error message
		relativePath := strings.TrimPrefix(cgroupPath, "/")
		fullPath := filepath.Join(consts.CgroupFilesystemPath, relativePath)
		t.Fatalf("cgroup path %q (full path: %q) still exists after realm purge", cgroupPath, fullPath)
	}

	// Step 7: Verify realm does NOT appear in list
	if verifyRealmInList(t, host, realmName) {
		t.Fatalf("realm %q still appears in realm list after purge", realmName)
	}

	// Step 8: Verify individual get FAILS (returns non-zero exit code)
	args = append(buildKukeDaemonArgs(host), "get", "realm", realmName, "--output", "json")
	exitCode, _, _ := runBinary(t, nil, kuke, args...)
	if exitCode == 0 {
		t.Fatalf("expected get realm to fail after purge, but got exit code 0")
	}

	// Note: Purge performs comprehensive cleanup beyond standard delete:
	// - CNI resources cleanup
	// - Orphaned containers cleanup
	// These are handled internally by the purge operation

	// Cleanup runs automatically via t.Cleanup()
}
