package api

import (
	"os"
	"path/filepath"
	"testing"
	"time"
)

func TestSessionHash(t *testing.T) {
	tests := []struct {
		name   string
		token1 string
		token2 string
		same   bool
	}{
		{
			name:   "same token produces same hash",
			token1: "test-token-123",
			token2: "test-token-123",
			same:   true,
		},
		{
			name:   "different tokens produce different hashes",
			token1: "token-a",
			token2: "token-b",
			same:   false,
		},
		{
			name:   "empty token produces consistent hash",
			token1: "",
			token2: "",
			same:   true,
		},
		{
			name:   "empty vs non-empty token",
			token1: "",
			token2: "something",
			same:   false,
		},
		{
			name:   "case sensitive",
			token1: "Token",
			token2: "token",
			same:   false,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			hash1 := sessionHash(tc.token1)
			hash2 := sessionHash(tc.token2)

			if tc.same && hash1 != hash2 {
				t.Errorf("sessionHash(%q) = %q, sessionHash(%q) = %q; want same", tc.token1, hash1, tc.token2, hash2)
			}
			if !tc.same && hash1 == hash2 {
				t.Errorf("sessionHash(%q) and sessionHash(%q) produced same hash %q; want different", tc.token1, tc.token2, hash1)
			}
		})
	}
}

func TestSessionHash_Format(t *testing.T) {
	hash := sessionHash("test-token")

	// SHA256 produces 64 hex characters
	if len(hash) != 64 {
		t.Errorf("sessionHash length = %d, want 64", len(hash))
	}

	// Should only contain hex characters
	for _, c := range hash {
		if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
			t.Errorf("sessionHash contains non-hex character: %c", c)
		}
	}
}

func TestSessionHash_Deterministic(t *testing.T) {
	token := "reproducible-token-value"

	// Call multiple times
	results := make([]string, 100)
	for i := range results {
		results[i] = sessionHash(token)
	}

	// All results should be identical
	for i := 1; i < len(results); i++ {
		if results[i] != results[0] {
			t.Errorf("sessionHash call %d returned %q, want %q", i, results[i], results[0])
		}
	}
}

func TestSessionData_Fields(t *testing.T) {
	now := time.Now()
	duration := 24 * time.Hour

	data := SessionData{
		ExpiresAt:        now.Add(duration),
		CreatedAt:        now,
		UserAgent:        "Mozilla/5.0",
		IP:               "192.168.1.100",
		OriginalDuration: duration,
	}

	if data.ExpiresAt.Before(data.CreatedAt) {
		t.Error("ExpiresAt should be after CreatedAt")
	}
	if data.UserAgent != "Mozilla/5.0" {
		t.Errorf("UserAgent = %q, want Mozilla/5.0", data.UserAgent)
	}
	if data.IP != "192.168.1.100" {
		t.Errorf("IP = %q, want 192.168.1.100", data.IP)
	}
	if data.OriginalDuration != duration {
		t.Errorf("OriginalDuration = %v, want %v", data.OriginalDuration, duration)
	}
}

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

	store := &SessionStore{
		sessions: make(map[string]*SessionData),
		dataPath: tmpDir,
		stopChan: make(chan bool),
	}

	token := "test-session-token"
	duration := time.Hour

	// Create session
	store.CreateSession(token, duration, "TestAgent", "127.0.0.1")

	// Validate session
	if !store.ValidateSession(token) {
		t.Error("ValidateSession returned false for valid session")
	}

	// Verify internal state
	store.mu.RLock()
	key := sessionHash(token)
	session, exists := store.sessions[key]
	store.mu.RUnlock()

	if !exists {
		t.Fatal("Session not found in store")
	}
	if session.UserAgent != "TestAgent" {
		t.Errorf("UserAgent = %q, want TestAgent", session.UserAgent)
	}
	if session.IP != "127.0.0.1" {
		t.Errorf("IP = %q, want 127.0.0.1", session.IP)
	}
	if session.OriginalDuration != duration {
		t.Errorf("OriginalDuration = %v, want %v", session.OriginalDuration, duration)
	}
}

func TestSessionStore_ValidateSession_NonExistent(t *testing.T) {
	store := &SessionStore{
		sessions: make(map[string]*SessionData),
		dataPath: t.TempDir(),
		stopChan: make(chan bool),
	}

	if store.ValidateSession("nonexistent-token") {
		t.Error("ValidateSession returned true for nonexistent session")
	}
}

func TestSessionStore_ValidateSession_Expired(t *testing.T) {
	store := &SessionStore{
		sessions: make(map[string]*SessionData),
		dataPath: t.TempDir(),
		stopChan: make(chan bool),
	}

	token := "expired-token"
	key := sessionHash(token)

	// Add an already-expired session
	store.mu.Lock()
	store.sessions[key] = &SessionData{
		ExpiresAt: time.Now().Add(-time.Hour), // Expired an hour ago
		CreatedAt: time.Now().Add(-2 * time.Hour),
	}
	store.mu.Unlock()

	if store.ValidateSession(token) {
		t.Error("ValidateSession returned true for expired session")
	}
}

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

	store := &SessionStore{
		sessions: make(map[string]*SessionData),
		dataPath: tmpDir,
		stopChan: make(chan bool),
	}

	token := "delete-me-token"

	// Create session
	store.CreateSession(token, time.Hour, "", "")

	// Verify exists
	if !store.ValidateSession(token) {
		t.Fatal("Session should exist before deletion")
	}

	// Delete session
	store.DeleteSession(token)

	// Verify deleted
	if store.ValidateSession(token) {
		t.Error("Session should not exist after deletion")
	}
}

func TestSessionStore_ValidateAndExtendSession(t *testing.T) {
	store := &SessionStore{
		sessions: make(map[string]*SessionData),
		dataPath: t.TempDir(),
		stopChan: make(chan bool),
	}

	token := "extend-me-token"
	duration := time.Hour

	// Create session
	store.CreateSession(token, duration, "", "")

	// Get original expiry
	store.mu.RLock()
	key := sessionHash(token)
	originalExpiry := store.sessions[key].ExpiresAt
	store.mu.RUnlock()

	// Wait a moment
	time.Sleep(10 * time.Millisecond)

	// Validate and extend
	if !store.ValidateAndExtendSession(token) {
		t.Error("ValidateAndExtendSession returned false for valid session")
	}

	// Verify expiry was extended
	store.mu.RLock()
	newExpiry := store.sessions[key].ExpiresAt
	store.mu.RUnlock()

	if !newExpiry.After(originalExpiry) {
		t.Error("Session expiry was not extended")
	}
}

func TestSessionStore_ValidateAndExtendSession_NonExistent(t *testing.T) {
	store := &SessionStore{
		sessions: make(map[string]*SessionData),
		dataPath: t.TempDir(),
		stopChan: make(chan bool),
	}

	if store.ValidateAndExtendSession("nonexistent-token") {
		t.Error("ValidateAndExtendSession returned true for nonexistent session")
	}
}

func TestSessionStore_ValidateAndExtendSession_Expired(t *testing.T) {
	store := &SessionStore{
		sessions: make(map[string]*SessionData),
		dataPath: t.TempDir(),
		stopChan: make(chan bool),
	}

	token := "expired-extend-token"
	key := sessionHash(token)

	// Add an already-expired session
	store.mu.Lock()
	store.sessions[key] = &SessionData{
		ExpiresAt:        time.Now().Add(-time.Hour),
		CreatedAt:        time.Now().Add(-2 * time.Hour),
		OriginalDuration: time.Hour,
	}
	store.mu.Unlock()

	if store.ValidateAndExtendSession(token) {
		t.Error("ValidateAndExtendSession returned true for expired session")
	}
}

func TestSessionStore_ValidateAndExtendSession_ZeroDuration(t *testing.T) {
	store := &SessionStore{
		sessions: make(map[string]*SessionData),
		dataPath: t.TempDir(),
		stopChan: make(chan bool),
	}

	token := "zero-duration-token"
	key := sessionHash(token)

	// Add session with zero original duration
	store.mu.Lock()
	store.sessions[key] = &SessionData{
		ExpiresAt:        time.Now().Add(time.Hour),
		CreatedAt:        time.Now(),
		OriginalDuration: 0, // Zero duration - should not extend
	}
	store.mu.Unlock()

	originalExpiry := store.sessions[key].ExpiresAt

	// Validate and extend
	if !store.ValidateAndExtendSession(token) {
		t.Error("ValidateAndExtendSession returned false for valid session")
	}

	// Expiry should not change (zero duration means no sliding)
	store.mu.RLock()
	newExpiry := store.sessions[key].ExpiresAt
	store.mu.RUnlock()

	if !newExpiry.Equal(originalExpiry) {
		t.Error("Session with zero OriginalDuration should not have expiry extended")
	}
}

func TestSessionStore_MultipleSessions(t *testing.T) {
	store := &SessionStore{
		sessions: make(map[string]*SessionData),
		dataPath: t.TempDir(),
		stopChan: make(chan bool),
	}

	tokens := []string{"token-1", "token-2", "token-3"}

	// Create multiple sessions
	for _, token := range tokens {
		store.CreateSession(token, time.Hour, "Agent-"+token, "")
	}

	// All should be valid
	for _, token := range tokens {
		if !store.ValidateSession(token) {
			t.Errorf("ValidateSession(%q) = false, want true", token)
		}
	}

	// Delete middle one
	store.DeleteSession("token-2")

	// First and last should still be valid
	if !store.ValidateSession("token-1") {
		t.Error("token-1 should still be valid")
	}
	if store.ValidateSession("token-2") {
		t.Error("token-2 should be deleted")
	}
	if !store.ValidateSession("token-3") {
		t.Error("token-3 should still be valid")
	}
}

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

	// Create store and add session
	store1 := &SessionStore{
		sessions: make(map[string]*SessionData),
		dataPath: tmpDir,
		stopChan: make(chan bool),
	}

	token := "persistent-token"
	store1.CreateSession(token, time.Hour, "PersistAgent", "10.0.0.1")

	// Verify file was created
	sessionsFile := filepath.Join(tmpDir, "sessions.json")
	if _, err := os.Stat(sessionsFile); os.IsNotExist(err) {
		t.Fatal("sessions.json was not created")
	}

	// Create new store that should load from disk
	store2 := &SessionStore{
		sessions: make(map[string]*SessionData),
		dataPath: tmpDir,
		stopChan: make(chan bool),
	}
	store2.load()

	// Session should be loaded
	if !store2.ValidateSession(token) {
		t.Error("Session was not persisted and loaded correctly")
	}

	// Verify metadata was preserved
	store2.mu.RLock()
	key := sessionHash(token)
	session := store2.sessions[key]
	store2.mu.RUnlock()

	if session == nil {
		t.Fatal("Session not found after load")
	}
	if session.UserAgent != "PersistAgent" {
		t.Errorf("UserAgent = %q, want PersistAgent", session.UserAgent)
	}
	if session.IP != "10.0.0.1" {
		t.Errorf("IP = %q, want 10.0.0.1", session.IP)
	}
}

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

	// Manually create a sessions file with an expired session
	store := &SessionStore{
		sessions: make(map[string]*SessionData),
		dataPath: tmpDir,
		stopChan: make(chan bool),
	}

	token := "already-expired"
	key := sessionHash(token)

	// Add expired session
	store.mu.Lock()
	store.sessions[key] = &SessionData{
		ExpiresAt: time.Now().Add(-time.Hour),
		CreatedAt: time.Now().Add(-2 * time.Hour),
	}
	store.mu.Unlock()
	store.save()

	// Create new store and load
	store2 := &SessionStore{
		sessions: make(map[string]*SessionData),
		dataPath: tmpDir,
		stopChan: make(chan bool),
	}
	store2.load()

	// Expired session should not be loaded
	store2.mu.RLock()
	_, exists := store2.sessions[key]
	store2.mu.RUnlock()

	if exists {
		t.Error("Expired session should not be loaded from disk")
	}
}

func TestSessionStore_Cleanup(t *testing.T) {
	store := &SessionStore{
		sessions: make(map[string]*SessionData),
		dataPath: t.TempDir(),
		stopChan: make(chan bool),
	}

	// Add a valid and an expired session
	validKey := sessionHash("valid-session")
	expiredKey := sessionHash("expired-session")

	store.mu.Lock()
	store.sessions[validKey] = &SessionData{
		ExpiresAt: time.Now().Add(time.Hour),
		CreatedAt: time.Now(),
	}
	store.sessions[expiredKey] = &SessionData{
		ExpiresAt: time.Now().Add(-time.Hour),
		CreatedAt: time.Now().Add(-2 * time.Hour),
	}
	store.mu.Unlock()

	// Run cleanup
	store.cleanup()

	// Valid session should remain
	store.mu.RLock()
	_, validExists := store.sessions[validKey]
	_, expiredExists := store.sessions[expiredKey]
	store.mu.RUnlock()

	if !validExists {
		t.Error("Valid session was incorrectly removed during cleanup")
	}
	if expiredExists {
		t.Error("Expired session was not removed during cleanup")
	}
}

func TestSessionPersistedStruct_Fields(t *testing.T) {
	now := time.Now()
	duration := 2 * time.Hour

	persisted := sessionPersisted{
		Key:              "abc123",
		ExpiresAt:        now.Add(duration),
		CreatedAt:        now,
		UserAgent:        "TestUA",
		IP:               "1.2.3.4",
		OriginalDuration: duration,
	}

	if persisted.Key != "abc123" {
		t.Errorf("Key = %q, want abc123", persisted.Key)
	}
	if persisted.UserAgent != "TestUA" {
		t.Errorf("UserAgent = %q, want TestUA", persisted.UserAgent)
	}
	if persisted.IP != "1.2.3.4" {
		t.Errorf("IP = %q, want 1.2.3.4", persisted.IP)
	}
	if persisted.OriginalDuration != duration {
		t.Errorf("OriginalDuration = %v, want %v", persisted.OriginalDuration, duration)
	}
}

func TestSessionStore_SaveUnsafe_MkdirAllError(t *testing.T) {
	// Create a file where the directory should be - MkdirAll will fail
	tmpDir := t.TempDir()
	blockedPath := filepath.Join(tmpDir, "blocked")

	// Create a file at the path where dataPath should be
	if err := os.WriteFile(blockedPath, []byte("blocking file"), 0644); err != nil {
		t.Fatalf("failed to create blocking file: %v", err)
	}

	store := &SessionStore{
		sessions: make(map[string]*SessionData),
		dataPath: blockedPath, // This is a file, not a directory
		stopChan: make(chan bool),
	}

	// Add a session to save
	key := sessionHash("test-token")
	store.sessions[key] = &SessionData{
		ExpiresAt: time.Now().Add(time.Hour),
		CreatedAt: time.Now(),
	}

	// saveUnsafe should handle error gracefully (logs but doesn't panic)
	store.saveUnsafe()

	// Verify the blocking file still exists (wasn't overwritten)
	data, err := os.ReadFile(blockedPath)
	if err != nil {
		t.Fatalf("blocking file was removed: %v", err)
	}
	if string(data) != "blocking file" {
		t.Error("blocking file was modified")
	}
}

func TestSessionStore_SaveUnsafe_WriteFileError(t *testing.T) {
	// Create a read-only directory to prevent file creation
	tmpDir := t.TempDir()
	readOnlyDir := filepath.Join(tmpDir, "readonly")

	if err := os.Mkdir(readOnlyDir, 0755); err != nil {
		t.Fatalf("failed to create readonly dir: %v", err)
	}

	store := &SessionStore{
		sessions: make(map[string]*SessionData),
		dataPath: readOnlyDir,
		stopChan: make(chan bool),
	}

	// Add a session
	key := sessionHash("test-token")
	store.sessions[key] = &SessionData{
		ExpiresAt: time.Now().Add(time.Hour),
		CreatedAt: time.Now(),
	}

	// Make directory read-only after it exists (MkdirAll succeeds, WriteFile fails)
	if err := os.Chmod(readOnlyDir, 0555); err != nil {
		t.Fatalf("failed to make dir readonly: %v", err)
	}
	t.Cleanup(func() {
		os.Chmod(readOnlyDir, 0755) // Restore for cleanup
	})

	// saveUnsafe should handle error gracefully (logs but doesn't panic)
	store.saveUnsafe()

	// Verify no file was created
	sessionsFile := filepath.Join(readOnlyDir, "sessions.json")
	if _, err := os.Stat(sessionsFile); err == nil {
		t.Error("sessions file should not exist after write failure")
	}
}

func TestSessionStore_SaveUnsafe_RenameError(t *testing.T) {
	// Create a directory at the target path to cause rename error
	tmpDir := t.TempDir()

	// Create a directory at the exact path where sessions.json should go
	sessionsFilePath := filepath.Join(tmpDir, "sessions.json")
	if err := os.Mkdir(sessionsFilePath, 0755); err != nil {
		t.Fatalf("failed to create blocking directory: %v", err)
	}

	store := &SessionStore{
		sessions: make(map[string]*SessionData),
		dataPath: tmpDir,
		stopChan: make(chan bool),
	}

	// Add a session
	key := sessionHash("test-token")
	store.sessions[key] = &SessionData{
		ExpiresAt: time.Now().Add(time.Hour),
		CreatedAt: time.Now(),
	}

	// saveUnsafe should handle rename error gracefully
	store.saveUnsafe()

	// The blocking directory should still exist (rename failed)
	info, err := os.Stat(sessionsFilePath)
	if err != nil {
		t.Fatalf("blocking directory was removed: %v", err)
	}
	if !info.IsDir() {
		t.Error("blocking directory was replaced with a file")
	}
}

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

	// Create a directory where the sessions file should be - reading it will fail
	sessionsPath := filepath.Join(tmpDir, "sessions.json")
	if err := os.Mkdir(sessionsPath, 0755); err != nil {
		t.Fatalf("failed to create directory: %v", err)
	}

	store := &SessionStore{
		sessions: make(map[string]*SessionData),
		dataPath: tmpDir,
		stopChan: make(chan bool),
	}

	// Should not panic and should log error
	store.load()

	if len(store.sessions) != 0 {
		t.Errorf("store should be empty after read error, got %d sessions", len(store.sessions))
	}
}

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

	// Write invalid JSON that won't match either format
	sessionsFile := filepath.Join(tmpDir, "sessions.json")
	if err := os.WriteFile(sessionsFile, []byte("not valid json at all"), 0600); err != nil {
		t.Fatalf("failed to write invalid JSON: %v", err)
	}

	store := &SessionStore{
		sessions: make(map[string]*SessionData),
		dataPath: tmpDir,
		stopChan: make(chan bool),
	}

	// Should not panic
	store.load()

	if len(store.sessions) != 0 {
		t.Errorf("store should be empty after loading invalid JSON, got %d sessions", len(store.sessions))
	}
}

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

	// Create legacy format JSON (map[token]sessionData) with snake_case fields
	// The legacy format uses raw tokens as keys
	legacyJSON := `{
		"raw-token-1": {"expires_at": "2099-12-31T23:59:59Z", "created_at": "2024-01-01T00:00:00Z", "user_agent": "Agent1", "ip": "1.1.1.1"},
		"raw-token-2": {"expires_at": "2099-12-31T23:59:59Z", "created_at": "2024-01-01T00:00:00Z", "user_agent": "Agent2", "ip": "2.2.2.2"},
		"raw-token-expired": {"expires_at": "2020-01-01T00:00:00Z", "created_at": "2019-01-01T00:00:00Z", "user_agent": "ExpiredAgent", "ip": "3.3.3.3"}
	}`
	sessionsFile := filepath.Join(tmpDir, "sessions.json")
	if err := os.WriteFile(sessionsFile, []byte(legacyJSON), 0600); err != nil {
		t.Fatalf("failed to write legacy format JSON: %v", err)
	}

	store := &SessionStore{
		sessions: make(map[string]*SessionData),
		dataPath: tmpDir,
		stopChan: make(chan bool),
	}

	store.load()

	// Should load the two non-expired sessions (raw-token-1 and raw-token-2)
	// The expired one (raw-token-expired) should be skipped
	if len(store.sessions) != 2 {
		t.Errorf("expected 2 sessions from legacy format, got %d", len(store.sessions))
	}

	// Verify sessions are hashed and can be validated
	// The legacy format stores raw tokens as keys, which get hashed during migration
	if !store.ValidateSession("raw-token-1") {
		t.Error("should validate session for raw-token-1 after legacy migration")
	}
	if !store.ValidateSession("raw-token-2") {
		t.Error("should validate session for raw-token-2 after legacy migration")
	}
	// Expired token should not be valid
	if store.ValidateSession("raw-token-expired") {
		t.Error("expired session should not be loaded from legacy format")
	}
}
