package api

import (
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"github.com/rcourtman/pulse-go-rewrite/internal/config"
)

func TestLoadOrCreateBootstrapToken_EmptyDataPath(t *testing.T) {
	tests := []struct {
		name     string
		dataPath string
	}{
		{"empty string", ""},
		{"whitespace only", "   "},
		{"tabs and spaces", "  \t  "},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			token, created, fullPath, err := loadOrCreateBootstrapToken(tt.dataPath)
			if err == nil {
				t.Error("expected error for empty data path, got nil")
			}
			if token != "" {
				t.Errorf("expected empty token, got %q", token)
			}
			if created {
				t.Error("expected created=false")
			}
			if fullPath != "" {
				t.Errorf("expected empty fullPath, got %q", fullPath)
			}
			if err.Error() != "data path required for bootstrap token" {
				t.Errorf("unexpected error message: %v", err)
			}
		})
	}
}

func TestLoadOrCreateBootstrapToken_MkdirAllFailure(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("permission tests not reliable on Windows")
	}
	if os.Getuid() == 0 {
		t.Skip("cannot test permission errors as root")
	}

	// Create a temp directory
	tmpDir := t.TempDir()

	// Create a file where we want a directory (MkdirAll will fail)
	blockingFile := filepath.Join(tmpDir, "blocker")
	if err := os.WriteFile(blockingFile, []byte("block"), 0o600); err != nil {
		t.Fatalf("failed to create blocking file: %v", err)
	}

	// Try to use the file path as a directory path
	token, created, fullPath, err := loadOrCreateBootstrapToken(blockingFile)
	if err == nil {
		t.Error("expected error when MkdirAll fails, got nil")
	}
	if token != "" {
		t.Errorf("expected empty token, got %q", token)
	}
	if created {
		t.Error("expected created=false")
	}
	if fullPath != "" {
		t.Errorf("expected empty fullPath, got %q", fullPath)
	}
}

func TestLoadOrCreateBootstrapToken_WriteFileFailure(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("permission tests not reliable on Windows")
	}
	if os.Getuid() == 0 {
		t.Skip("cannot test permission errors as root")
	}

	// Create a temp directory with no write permissions
	tmpDir := t.TempDir()
	readOnlyDir := filepath.Join(tmpDir, "readonly")
	if err := os.MkdirAll(readOnlyDir, 0o700); err != nil {
		t.Fatalf("failed to create readonly dir: %v", err)
	}

	// Remove write permissions
	if err := os.Chmod(readOnlyDir, 0o500); err != nil {
		t.Fatalf("failed to chmod dir: %v", err)
	}
	// Restore permissions for cleanup
	t.Cleanup(func() {
		os.Chmod(readOnlyDir, 0o700)
	})

	token, created, fullPath, err := loadOrCreateBootstrapToken(readOnlyDir)
	if err == nil {
		t.Error("expected error when WriteFile fails, got nil")
	}
	if token != "" {
		t.Errorf("expected empty token, got %q", token)
	}
	if created {
		t.Error("expected created=false")
	}
	// fullPath should be set even on write failure (path was computed before write)
	expectedPath := filepath.Join(readOnlyDir, bootstrapTokenFilename)
	if fullPath != expectedPath {
		t.Errorf("expected fullPath=%q, got %q", expectedPath, fullPath)
	}
}

func TestLoadOrCreateBootstrapToken_EmptyFileContents(t *testing.T) {
	tmpDir := t.TempDir()

	// Create an empty bootstrap token file
	tokenPath := filepath.Join(tmpDir, bootstrapTokenFilename)
	if err := os.WriteFile(tokenPath, []byte(""), 0o600); err != nil {
		t.Fatalf("failed to create empty token file: %v", err)
	}

	token, created, fullPath, err := loadOrCreateBootstrapToken(tmpDir)
	if err == nil {
		t.Error("expected error for empty file contents, got nil")
	}
	if token != "" {
		t.Errorf("expected empty token, got %q", token)
	}
	if created {
		t.Error("expected created=false")
	}
	if fullPath != tokenPath {
		t.Errorf("expected fullPath=%q, got %q", tokenPath, fullPath)
	}
	if err.Error() != "bootstrap token file is empty" {
		t.Errorf("unexpected error message: %v", err)
	}
}

func TestLoadOrCreateBootstrapToken_WhitespaceOnlyFileContents(t *testing.T) {
	tmpDir := t.TempDir()

	// Create a bootstrap token file with only whitespace
	tokenPath := filepath.Join(tmpDir, bootstrapTokenFilename)
	if err := os.WriteFile(tokenPath, []byte("   \n\t  \n"), 0o600); err != nil {
		t.Fatalf("failed to create whitespace-only token file: %v", err)
	}

	token, created, fullPath, err := loadOrCreateBootstrapToken(tmpDir)
	if err == nil {
		t.Error("expected error for whitespace-only file contents, got nil")
	}
	if token != "" {
		t.Errorf("expected empty token, got %q", token)
	}
	if created {
		t.Error("expected created=false")
	}
	if fullPath != tokenPath {
		t.Errorf("expected fullPath=%q, got %q", tokenPath, fullPath)
	}
	if err.Error() != "bootstrap token file is empty" {
		t.Errorf("unexpected error message: %v", err)
	}
}

func TestLoadOrCreateBootstrapToken_ReadFileFailure(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("permission tests not reliable on Windows")
	}
	if os.Getuid() == 0 {
		t.Skip("cannot test permission errors as root")
	}

	tmpDir := t.TempDir()

	// Create a token file with no read permissions
	tokenPath := filepath.Join(tmpDir, bootstrapTokenFilename)
	if err := os.WriteFile(tokenPath, []byte("sometoken"), 0o000); err != nil {
		t.Fatalf("failed to create unreadable token file: %v", err)
	}
	t.Cleanup(func() {
		os.Chmod(tokenPath, 0o600)
	})

	token, created, fullPath, err := loadOrCreateBootstrapToken(tmpDir)
	if err == nil {
		t.Error("expected error when ReadFile fails, got nil")
	}
	if token != "" {
		t.Errorf("expected empty token, got %q", token)
	}
	if created {
		t.Error("expected created=false")
	}
	if fullPath != tokenPath {
		t.Errorf("expected fullPath=%q, got %q", tokenPath, fullPath)
	}
}

func TestLoadOrCreateBootstrapToken_Success_NewToken(t *testing.T) {
	tmpDir := t.TempDir()

	token, created, fullPath, err := loadOrCreateBootstrapToken(tmpDir)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if token == "" {
		t.Error("expected non-empty token")
	}
	if !created {
		t.Error("expected created=true for new token")
	}
	expectedPath := filepath.Join(tmpDir, bootstrapTokenFilename)
	if fullPath != expectedPath {
		t.Errorf("expected fullPath=%q, got %q", expectedPath, fullPath)
	}

	// Verify the token was written to disk
	data, err := os.ReadFile(fullPath)
	if err != nil {
		t.Fatalf("failed to read token file: %v", err)
	}
	// Token is written with trailing newline
	if string(data) != token+"\n" {
		t.Errorf("token file contents mismatch: got %q, want %q", string(data), token+"\n")
	}
}

func TestLoadOrCreateBootstrapToken_Success_ExistingToken(t *testing.T) {
	tmpDir := t.TempDir()

	// Pre-create a token file
	existingToken := "myexistingtoken123"
	tokenPath := filepath.Join(tmpDir, bootstrapTokenFilename)
	if err := os.WriteFile(tokenPath, []byte(existingToken+"\n"), 0o600); err != nil {
		t.Fatalf("failed to create existing token file: %v", err)
	}

	token, created, fullPath, err := loadOrCreateBootstrapToken(tmpDir)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if token != existingToken {
		t.Errorf("expected token=%q, got %q", existingToken, token)
	}
	if created {
		t.Error("expected created=false for existing token")
	}
	if fullPath != tokenPath {
		t.Errorf("expected fullPath=%q, got %q", tokenPath, fullPath)
	}
}

func TestGenerateBootstrapToken(t *testing.T) {
	// Test that tokens are generated
	token, err := generateBootstrapToken()
	if err != nil {
		t.Fatalf("generateBootstrapToken() error: %v", err)
	}
	if token == "" {
		t.Error("generateBootstrapToken() returned empty string")
	}

	// Test token length (24 bytes = 48 hex characters)
	if len(token) != 48 {
		t.Errorf("generateBootstrapToken() length = %d, want 48", len(token))
	}

	// Test that tokens are unique
	tokens := make(map[string]bool)
	for i := 0; i < 100; i++ {
		tok, err := generateBootstrapToken()
		if err != nil {
			t.Fatalf("generateBootstrapToken() error on iteration %d: %v", i, err)
		}
		if tokens[tok] {
			t.Errorf("generateBootstrapToken() generated duplicate token: %s", tok)
		}
		tokens[tok] = true
	}

	// Test that token is valid hex
	for _, c := range token {
		if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
			t.Errorf("generateBootstrapToken() contains non-hex character: %c", c)
		}
	}
}

func TestBootstrapTokenValid(t *testing.T) {
	t.Run("nil router returns false", func(t *testing.T) {
		var r *Router = nil
		if r.bootstrapTokenValid("anything") {
			t.Error("expected false for nil router")
		}
	})

	t.Run("empty hash returns false", func(t *testing.T) {
		r := &Router{bootstrapTokenHash: ""}
		if r.bootstrapTokenValid("anything") {
			t.Error("expected false when bootstrapTokenHash is empty")
		}
	})

	t.Run("empty token returns false", func(t *testing.T) {
		// Generate a token and create a router with its hash
		token, err := generateBootstrapToken()
		if err != nil {
			t.Fatalf("generateBootstrapToken() error: %v", err)
		}
		r := &Router{}
		// Create a token and store its hash - use loadOrCreateBootstrapToken to get a hash indirectly
		tmpDir := t.TempDir()
		loadedToken, _, _, err := loadOrCreateBootstrapToken(tmpDir)
		if err != nil {
			t.Fatalf("loadOrCreateBootstrapToken() error: %v", err)
		}
		// Since we can't directly access HashAPIToken, we need to use initializeBootstrapToken
		// Instead, let's test with a known hash by using the auth package
		_ = token
		_ = loadedToken
		// For this test, we just need any non-empty hash
		r.bootstrapTokenHash = "somehash"

		if r.bootstrapTokenValid("") {
			t.Error("expected false for empty token")
		}
		if r.bootstrapTokenValid("   ") {
			t.Error("expected false for whitespace-only token")
		}
	})

	t.Run("valid token returns true", func(t *testing.T) {
		tmpDir := t.TempDir()
		token, _, _, err := loadOrCreateBootstrapToken(tmpDir)
		if err != nil {
			t.Fatalf("loadOrCreateBootstrapToken() error: %v", err)
		}

		// Create a router that simulates having loaded the token
		// We need to use the auth package to hash the token
		cfg := &config.Config{DataPath: tmpDir}
		r := &Router{config: cfg}
		r.initializeBootstrapToken()

		if !r.bootstrapTokenValid(token) {
			t.Error("expected true for valid token")
		}
	})

	t.Run("invalid token returns false", func(t *testing.T) {
		tmpDir := t.TempDir()
		_, _, _, err := loadOrCreateBootstrapToken(tmpDir)
		if err != nil {
			t.Fatalf("loadOrCreateBootstrapToken() error: %v", err)
		}

		cfg := &config.Config{DataPath: tmpDir}
		r := &Router{config: cfg}
		r.initializeBootstrapToken()

		if r.bootstrapTokenValid("wrongtoken") {
			t.Error("expected false for wrong token")
		}
	})

	t.Run("token with whitespace is trimmed", func(t *testing.T) {
		tmpDir := t.TempDir()
		token, _, _, err := loadOrCreateBootstrapToken(tmpDir)
		if err != nil {
			t.Fatalf("loadOrCreateBootstrapToken() error: %v", err)
		}

		cfg := &config.Config{DataPath: tmpDir}
		r := &Router{config: cfg}
		r.initializeBootstrapToken()

		// Token with leading/trailing whitespace should still validate
		if !r.bootstrapTokenValid("  " + token + "  ") {
			t.Error("expected true for token with surrounding whitespace")
		}
	})
}

func TestInitializeBootstrapToken_NilRouter(t *testing.T) {
	var r *Router = nil
	// Should not panic
	r.initializeBootstrapToken()
}

func TestInitializeBootstrapToken_NilConfig(t *testing.T) {
	r := &Router{config: nil}
	// Should not panic
	r.initializeBootstrapToken()
}

func TestInitializeBootstrapToken_LoadOrCreateFails(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("permission tests not reliable on Windows")
	}
	if os.Getuid() == 0 {
		t.Skip("cannot test permission errors as root")
	}

	// Create a temp directory with a file where the data path expects a directory
	tmpDir := t.TempDir()
	blockingFile := filepath.Join(tmpDir, "blocked")
	if err := os.WriteFile(blockingFile, []byte("block"), 0o600); err != nil {
		t.Fatalf("failed to create blocking file: %v", err)
	}

	// DataPath points to a file, so MkdirAll will fail inside loadOrCreateBootstrapToken
	cfg := &config.Config{DataPath: blockingFile}
	r := &Router{config: cfg}

	// Should not panic, but should fail and leave bootstrapTokenHash empty
	r.initializeBootstrapToken()

	if r.bootstrapTokenHash != "" {
		t.Errorf("expected empty bootstrapTokenHash when loadOrCreateBootstrapToken fails, got %q", r.bootstrapTokenHash)
	}
	if r.bootstrapTokenPath != "" {
		t.Errorf("expected empty bootstrapTokenPath when loadOrCreateBootstrapToken fails, got %q", r.bootstrapTokenPath)
	}
}

func TestInitializeBootstrapToken_OIDCEnabled(t *testing.T) {
	tmpDir := t.TempDir()

	// Create a bootstrap token file first
	tokenPath := filepath.Join(tmpDir, bootstrapTokenFilename)
	if err := os.WriteFile(tokenPath, []byte("testtoken\n"), 0o600); err != nil {
		t.Fatalf("failed to create token file: %v", err)
	}

	cfg := &config.Config{
		DataPath: tmpDir,
		OIDC: &config.OIDCConfig{
			Enabled: true,
		},
	}
	r := &Router{
		config:             cfg,
		bootstrapTokenPath: tokenPath, // Set path so clearBootstrapToken can delete the file
	}
	r.initializeBootstrapToken()

	// Token should be cleared when OIDC is enabled
	if r.bootstrapTokenHash != "" {
		t.Error("expected empty bootstrapTokenHash when OIDC is enabled")
	}

	// Token file should be removed
	if _, err := os.Stat(tokenPath); !os.IsNotExist(err) {
		t.Error("expected token file to be deleted when OIDC is enabled")
	}
}

func TestHandleValidateBootstrapToken_InvalidJSON(t *testing.T) {
	tmpDir := t.TempDir()
	cfg := &config.Config{DataPath: tmpDir}
	r := &Router{config: cfg}
	r.initializeBootstrapToken()

	// Test invalid JSON body triggers json.Decode error
	req := httptest.NewRequest(http.MethodPost, "/api/security/validate-bootstrap-token", strings.NewReader("not valid json"))
	rr := httptest.NewRecorder()

	r.handleValidateBootstrapToken(rr, req)

	if rr.Code != http.StatusBadRequest {
		t.Errorf("expected 400 for invalid JSON, got %d", rr.Code)
	}
	if !strings.Contains(rr.Body.String(), "Invalid request payload") {
		t.Errorf("expected 'Invalid request payload' error, got %q", rr.Body.String())
	}
}

func TestClearBootstrapToken(t *testing.T) {
	t.Run("nil router does not panic", func(t *testing.T) {
		var r *Router = nil
		// Should not panic
		r.clearBootstrapToken()
	})

	t.Run("clears token hash and path", func(t *testing.T) {
		tmpDir := t.TempDir()
		_, _, tokenPath, err := loadOrCreateBootstrapToken(tmpDir)
		if err != nil {
			t.Fatalf("loadOrCreateBootstrapToken() error: %v", err)
		}

		r := &Router{
			bootstrapTokenHash: "somehash",
			bootstrapTokenPath: tokenPath,
		}

		r.clearBootstrapToken()

		if r.bootstrapTokenHash != "" {
			t.Errorf("expected empty bootstrapTokenHash, got %q", r.bootstrapTokenHash)
		}
		if r.bootstrapTokenPath != "" {
			t.Errorf("expected empty bootstrapTokenPath, got %q", r.bootstrapTokenPath)
		}

		// Verify file was deleted
		if _, err := os.Stat(tokenPath); !os.IsNotExist(err) {
			t.Error("expected token file to be deleted")
		}
	})

	t.Run("handles missing file gracefully", func(t *testing.T) {
		r := &Router{
			bootstrapTokenHash: "somehash",
			bootstrapTokenPath: "/nonexistent/path/token",
		}

		// Should not panic, should clear the hash and path
		r.clearBootstrapToken()

		if r.bootstrapTokenHash != "" {
			t.Errorf("expected empty bootstrapTokenHash, got %q", r.bootstrapTokenHash)
		}
		if r.bootstrapTokenPath != "" {
			t.Errorf("expected empty bootstrapTokenPath, got %q", r.bootstrapTokenPath)
		}
	})

	t.Run("handles empty path", func(t *testing.T) {
		r := &Router{
			bootstrapTokenHash: "somehash",
			bootstrapTokenPath: "",
		}

		// Should not panic
		r.clearBootstrapToken()

		if r.bootstrapTokenHash != "" {
			t.Errorf("expected empty bootstrapTokenHash, got %q", r.bootstrapTokenHash)
		}
	})

	t.Run("remove failure logs warning but clears fields", func(t *testing.T) {
		// Point to a directory with contents - os.Remove will fail with ENOTEMPTY
		tmpDir := t.TempDir()
		subFile := filepath.Join(tmpDir, "subfile")
		if err := os.WriteFile(subFile, []byte("test"), 0o600); err != nil {
			t.Fatalf("failed to create subfile: %v", err)
		}

		r := &Router{
			bootstrapTokenHash: "somehash",
			bootstrapTokenPath: tmpDir, // Point to directory, not file
		}

		// Should not panic and should clear fields even if remove fails
		r.clearBootstrapToken()

		if r.bootstrapTokenHash != "" {
			t.Errorf("expected empty bootstrapTokenHash, got %q", r.bootstrapTokenHash)
		}
		if r.bootstrapTokenPath != "" {
			t.Errorf("expected empty bootstrapTokenPath, got %q", r.bootstrapTokenPath)
		}
	})
}
