package workspace

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

	"github.com/stretchr/testify/require"
)

func TestInitializeMemoryLayoutCreatesScaffold(t *testing.T) {
	t.Parallel()

	workDir := t.TempDir()

	require.NoError(t, InitializeMemoryLayout(workDir, "Review Agent"))

	for _, rel := range []string{
		stateDirName,
		kbDirName,
		filepath.Join(kbDirName, "tasks"),
		archiveDirName,
		filepath.Join(archiveDirName, "tasks"),
	} {
		info, err := os.Stat(filepath.Join(workDir, rel))
		require.NoError(t, err, rel)
		require.True(t, info.IsDir(), rel)
	}

	memoryBytes, err := os.ReadFile(filepath.Join(workDir, memoryFileName))
	require.NoError(t, err)
	require.Contains(t, string(memoryBytes), "# Review Agent")
	require.Contains(t, string(memoryBytes), "## Active Context")
	require.Contains(t, string(memoryBytes), "First startup.")
	require.Contains(t, string(memoryBytes), "## Recovery Read Order")
	require.Contains(t, string(memoryBytes), activeTaskRelPath)
	require.Contains(t, string(memoryBytes), openLoopsRelPath)
	require.Contains(t, string(memoryBytes), kbIndexRelPath)
	require.Contains(t, string(memoryBytes), archiveCheckpointsPath)
	require.Contains(t, string(memoryBytes), "## Memory Map")
	require.Contains(t, string(memoryBytes), "`kb/tasks/<task-id>.md`")
	require.Contains(t, string(memoryBytes), "## Historical Fallback")
	require.Contains(t, string(memoryBytes), "## Portability Boundary")

	kbIndexBytes, err := os.ReadFile(filepath.Join(workDir, kbIndexRelPath))
	require.NoError(t, err)
	require.Contains(t, string(kbIndexBytes), "`kb/`")
	require.Contains(t, string(kbIndexBytes), "`kb/domain.md`")
	require.Contains(t, string(kbIndexBytes), "`kb/user-preferences.md`")
	require.Contains(t, string(kbIndexBytes), "`kb/channel-context.md`")
	require.Contains(t, string(kbIndexBytes), "the Role package")
	require.Contains(t, string(kbIndexBytes), "`notes/*`")

	var activeTask activeTaskState
	require.NoError(t, readJSONFile(filepath.Join(workDir, activeTaskRelPath), &activeTask))
	require.Equal(t, localMemorySchema, activeTask.SchemaVersion)
	require.NotEmpty(t, activeTask.UpdatedAt)
	require.Equal(t, "No active task recorded yet.", activeTask.CurrentObjective)
	require.Nil(t, activeTask.LastCheckpoint)
	require.Equal(t, []string{
		openLoopsRelPath,
		activeContextRelPath,
		kbIndexRelPath,
		archiveCheckpointsPath,
	}, activeTask.RelevantFiles)

	activeContextBytes, err := os.ReadFile(filepath.Join(workDir, activeContextRelPath))
	require.NoError(t, err)
	require.Contains(t, string(activeContextBytes), "# Active Context")
	require.Contains(t, string(activeContextBytes), "- Updated: ")
	require.Contains(t, string(activeContextBytes), "No runtime checkpoint recorded yet.")
	require.Contains(t, string(activeContextBytes), "`state/open-loops.json`")

	var openLoops openLoopsState
	require.NoError(t, readJSONFile(filepath.Join(workDir, openLoopsRelPath), &openLoops))
	require.Equal(t, localMemorySchema, openLoops.SchemaVersion)
	require.Empty(t, openLoops.Items)
	require.NotEmpty(t, openLoops.UpdatedAt)

	archiveBytes, err := os.ReadFile(filepath.Join(workDir, archiveCheckpointsPath))
	require.NoError(t, err)
	require.Equal(t, archiveCheckpointsHeader, string(archiveBytes))
}

func TestInitializeMemoryLayoutImportsLegacyKnowledgeNotesIntoKB(t *testing.T) {
	t.Parallel()

	workDir := t.TempDir()
	legacyDir := filepath.Join(workDir, legacyNotesDirName)
	require.NoError(t, os.MkdirAll(legacyDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(legacyDir, "domain.md"), []byte("# Domain\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(legacyDir, "user-preferences.md"), []byte("# User Preferences\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(legacyDir, "channels.md"), []byte("# Channel Context\n"), 0o644))

	require.NoError(t, InitializeMemoryLayout(workDir, "Review Agent"))

	domainBytes, err := os.ReadFile(filepath.Join(workDir, kbDomainRelPath))
	require.NoError(t, err)
	require.Equal(t, "# Domain\n", string(domainBytes))

	userPrefsBytes, err := os.ReadFile(filepath.Join(workDir, kbUserPrefsRelPath))
	require.NoError(t, err)
	require.Equal(t, "# User Preferences\n", string(userPrefsBytes))

	channelContextBytes, err := os.ReadFile(filepath.Join(workDir, kbChannelContextRelPath))
	require.NoError(t, err)
	require.Equal(t, "# Channel Context\n", string(channelContextBytes))
}

func TestInitializeMemoryLayoutDoesNotOverwriteKBFromLegacyNotes(t *testing.T) {
	t.Parallel()

	workDir := t.TempDir()
	legacyDir := filepath.Join(workDir, legacyNotesDirName)
	require.NoError(t, os.MkdirAll(legacyDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(legacyDir, "domain.md"), []byte("# Legacy Domain\n"), 0o644))
	require.NoError(t, os.MkdirAll(filepath.Join(workDir, kbDirName), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, kbDomainRelPath), []byte("# Canonical Domain\n"), 0o644))

	require.NoError(t, InitializeMemoryLayout(workDir, "Review Agent"))

	domainBytes, err := os.ReadFile(filepath.Join(workDir, kbDomainRelPath))
	require.NoError(t, err)
	require.Equal(t, "# Canonical Domain\n", string(domainBytes))
}

func TestRecordRecoveryCheckpointWritesStateContextAndArchive(t *testing.T) {
	t.Parallel()

	workDir := t.TempDir()
	checkpointTime := time.Date(2026, time.April, 24, 15, 4, 5, 0, time.UTC)

	require.NoError(t, RecordRecoveryCheckpoint(workDir, "reviewer", RecoveryCheckpoint{
		Timestamp:     checkpointTime,
		Trigger:       "wake",
		Summary:       "Verify the new local memory layout.",
		SessionID:     "sess_123",
		WakeTarget:    "#ops:thread-1",
		WakeMessageID: "msg_123",
	}))

	var activeTask activeTaskState
	require.NoError(t, readJSONFile(filepath.Join(workDir, activeTaskRelPath), &activeTask))
	require.Equal(t, localMemorySchema, activeTask.SchemaVersion)
	require.Equal(t, checkpointTime.Format(time.RFC3339), activeTask.UpdatedAt)
	require.Equal(t, "Verify the new local memory layout.", activeTask.CurrentObjective)
	require.Contains(t, activeTask.NextStep, "Recover the context needed to continue the current or pending work")
	require.NotContains(t, strings.ToLower(activeTask.NextStep), "re-read")
	require.NotNil(t, activeTask.LastCheckpoint)
	require.Equal(t, "wake", activeTask.LastCheckpoint.Trigger)
	require.Equal(t, "Verify the new local memory layout.", activeTask.LastCheckpoint.Summary)
	require.Equal(t, "sess_123", activeTask.LastCheckpoint.SessionID)
	require.Equal(t, "#ops:thread-1", activeTask.LastCheckpoint.WakeTarget)
	require.Equal(t, "msg_123", activeTask.LastCheckpoint.WakeMessageID)
	require.Equal(t, checkpointTime.Format(time.RFC3339), activeTask.LastCheckpoint.Timestamp)
	require.Equal(t, []string{
		openLoopsRelPath,
		activeContextRelPath,
		kbIndexRelPath,
		archiveCheckpointsPath,
	}, activeTask.RelevantFiles)

	activeContextBytes, err := os.ReadFile(filepath.Join(workDir, activeContextRelPath))
	require.NoError(t, err)
	require.Contains(t, string(activeContextBytes), "# Active Context")
	require.Contains(t, string(activeContextBytes), "- Updated: 2026-04-24T15:04:05Z")
	require.Contains(t, string(activeContextBytes), "- Trigger: wake")
	require.Contains(t, string(activeContextBytes), "- Summary: Verify the new local memory layout.")
	require.Contains(t, string(activeContextBytes), "- Open loops: `state/open-loops.json`")
	require.Contains(t, string(activeContextBytes), "- Session: `sess_123`")
	require.Contains(t, string(activeContextBytes), "- Wake target: `#ops:thread-1`")
	require.Contains(t, string(activeContextBytes), "- Wake message: `msg_123`")
	require.Contains(t, string(activeContextBytes), "`archive/checkpoints.md` (fallback only)")

	archiveBytes, err := os.ReadFile(filepath.Join(workDir, archiveCheckpointsPath))
	require.NoError(t, err)
	require.Contains(t, string(archiveBytes), "# Automatic Checkpoints")
	require.Contains(t, string(archiveBytes), "- 2026-04-24T15:04:05Z [wake] Verify the new local memory layout.")

	memoryBytes, err := os.ReadFile(filepath.Join(workDir, memoryFileName))
	require.NoError(t, err)
	require.Contains(t, string(memoryBytes), "Last runtime update: 2026-04-24T15:04:05Z [wake]")
	require.Contains(t, string(memoryBytes), "Current focus: Verify the new local memory layout.")
	require.Contains(t, string(memoryBytes), "Open loops: `state/open-loops.json`")
	require.Contains(t, string(memoryBytes), "## Memory Map")
	require.Contains(t, string(memoryBytes), "`kb/index.md`")
	require.LessOrEqual(t, len([]rune(strings.TrimSpace(string(memoryBytes)))), maxMemoryFileCharacters)
}

func TestRecordRecoveryCheckpointBoundsGeneratedMemoryIndex(t *testing.T) {
	t.Parallel()

	workDir := t.TempDir()
	longSummary := strings.Repeat("汲", maxMemoryFileCharacters*2)
	checkpointTime := time.Date(2026, time.April, 24, 15, 4, 5, 0, time.UTC)

	require.NoError(t, RecordRecoveryCheckpoint(workDir, "Review Agent", RecoveryCheckpoint{
		Timestamp: checkpointTime,
		Trigger:   "delivery",
		Summary:   longSummary,
	}))

	memoryBytes, err := os.ReadFile(filepath.Join(workDir, memoryFileName))
	require.NoError(t, err)
	memoryText := string(memoryBytes)
	require.LessOrEqual(t, len([]rune(strings.TrimSpace(memoryText))), maxMemoryFileCharacters)
	require.Contains(t, memoryText, "Long checkpoint summary omitted from MEMORY.md")
	require.Contains(t, memoryText, "full text in state/active-task.json")
	require.NotContains(t, memoryText, longSummary)
	require.NotContains(t, memoryText, "… (full text")

	var activeTask activeTaskState
	require.NoError(t, readJSONFile(filepath.Join(workDir, activeTaskRelPath), &activeTask))
	require.Equal(t, longSummary, activeTask.CurrentObjective)

	activeContextBytes, err := os.ReadFile(filepath.Join(workDir, activeContextRelPath))
	require.NoError(t, err)
	require.Contains(t, string(activeContextBytes), longSummary)
}

func TestRecordRecoveryCheckpointRegeneratesConciseDeliveryFocus(t *testing.T) {
	t.Parallel()

	workDir := t.TempDir()
	longBody := strings.Repeat("launch notes ", maxMemoryFileCharacters)
	summary := "New message from alice in #ops: " + longBody

	require.NoError(t, RecordRecoveryCheckpoint(workDir, "Review Agent", RecoveryCheckpoint{
		Timestamp: time.Date(2026, time.April, 24, 15, 4, 5, 0, time.UTC),
		Trigger:   "delivery",
		Summary:   summary,
	}))

	memoryBytes, err := os.ReadFile(filepath.Join(workDir, memoryFileName))
	require.NoError(t, err)
	memoryText := string(memoryBytes)
	require.LessOrEqual(t, len([]rune(strings.TrimSpace(memoryText))), maxMemoryFileCharacters)
	require.Contains(t, memoryText, "Current focus: New message from alice in #ops. Full text in state/active-task.json and state/active-context.md.")
	require.NotContains(t, memoryText, longBody)
	require.NotContains(t, memoryText, "… (full text")

	var activeTask activeTaskState
	require.NoError(t, readJSONFile(filepath.Join(workDir, activeTaskRelPath), &activeTask))
	require.Equal(t, summary, activeTask.CurrentObjective)
}

func TestRecordRecoveryCheckpointPreservesCustomMemoryMap(t *testing.T) {
	t.Parallel()

	workDir := t.TempDir()
	require.NoError(t, InitializeMemoryLayout(workDir, "Review Agent"))

	memoryPath := filepath.Join(workDir, memoryFileName)
	memoryBytes, err := os.ReadFile(memoryPath)
	require.NoError(t, err)
	customMap := "- `kb/project-conventions.md`: custom conventions learned from prior work."
	updated := replaceMarkdownSection(string(memoryBytes), "## Memory Map", customMap)
	require.NoError(t, os.WriteFile(memoryPath, []byte(updated), 0o644))

	require.NoError(t, RecordRecoveryCheckpoint(workDir, "Review Agent", RecoveryCheckpoint{
		Timestamp: time.Date(2026, time.April, 24, 17, 0, 0, 0, time.UTC),
		Trigger:   "resume",
		Summary:   "Resume without losing the custom memory map.",
	}))

	memoryBytes, err = os.ReadFile(memoryPath)
	require.NoError(t, err)
	require.Contains(t, string(memoryBytes), customMap)
	require.Contains(t, string(memoryBytes), "Current focus: Resume without losing the custom memory map.")
}

func TestInitializeMemoryLayoutCompactsOversizedDaemonActiveContext(t *testing.T) {
	t.Parallel()

	workDir := t.TempDir()
	oversized := "# Review Agent\n\n## Active Context\n- Current focus: " + strings.Repeat("界", maxMemoryFileCharacters+1)
	require.NoError(t, os.WriteFile(filepath.Join(workDir, memoryFileName), []byte(oversized), 0o644))

	report, err := InitializeMemoryLayoutWithReport(workDir, "Review Agent")
	require.NoError(t, err)
	require.Empty(t, report.Warning)

	memoryBytes, err := os.ReadFile(filepath.Join(workDir, memoryFileName))
	require.NoError(t, err)
	memoryText := string(memoryBytes)
	require.LessOrEqual(t, len([]rune(strings.TrimSpace(memoryText))), maxMemoryFileCharacters)
	require.Contains(t, memoryText, "First startup.")
	require.Contains(t, memoryText, activeTaskRelPath)
	require.NotContains(t, memoryText, strings.Repeat("界", 128))
}

func TestInitializeMemoryLayoutWarnsButAllowsOversizedMemoryIndex(t *testing.T) {
	t.Parallel()

	workDir := t.TempDir()
	oversized := "# Review Agent\n\n## Custom Notes\n" + strings.Repeat("界", maxMemoryFileCharacters+1)
	require.NoError(t, os.WriteFile(filepath.Join(workDir, memoryFileName), []byte(oversized), 0o644))

	report, err := InitializeMemoryLayoutWithReport(workDir, "Review Agent")
	require.NoError(t, err)
	require.True(t, report.MemoryIndexOversized)
	require.Greater(t, report.MemoryIndexCharacters, maxMemoryFileCharacters)
	require.Equal(t, maxMemoryFileCharacters, report.MemoryIndexLimit)
	require.Contains(t, report.Warning, "MEMORY.md is still over the recommended startup budget")
	require.Contains(t, report.Warning, "agent will continue to start")

	require.NoError(t, InitializeMemoryLayout(workDir, "Review Agent"))

	var activeTask activeTaskState
	require.NoError(t, readJSONFile(filepath.Join(workDir, activeTaskRelPath), &activeTask))
	require.Equal(t, localMemorySchema, activeTask.SchemaVersion)
}

func TestRecordRecoveryCheckpointAllowsOversizedMemoryIndexAndWritesState(t *testing.T) {
	t.Parallel()

	workDir := t.TempDir()
	oversized := "# Review Agent\n\n## Custom Notes\n" + strings.Repeat("界", maxMemoryFileCharacters+1)
	require.NoError(t, os.WriteFile(filepath.Join(workDir, memoryFileName), []byte(oversized), 0o644))

	checkpointTime := time.Date(2026, time.April, 24, 18, 0, 0, 0, time.UTC)
	require.NoError(t, RecordRecoveryCheckpoint(workDir, "Review Agent", RecoveryCheckpoint{
		Timestamp: checkpointTime,
		Trigger:   "resume",
		Summary:   "Resume even though MEMORY.md is still too large.",
	}))

	var activeTask activeTaskState
	require.NoError(t, readJSONFile(filepath.Join(workDir, activeTaskRelPath), &activeTask))
	require.Equal(t, "Resume even though MEMORY.md is still too large.", activeTask.CurrentObjective)
	require.NotNil(t, activeTask.LastCheckpoint)
	require.Equal(t, "resume", activeTask.LastCheckpoint.Trigger)

	archiveBytes, err := os.ReadFile(filepath.Join(workDir, archiveCheckpointsPath))
	require.NoError(t, err)
	require.Contains(t, string(archiveBytes), "- 2026-04-24T18:00:00Z [resume] Resume even though MEMORY.md is still too large.")
}

func TestRecordRecoveryCheckpointImportsLegacyCheckpointsIntoArchive(t *testing.T) {
	t.Parallel()

	workDir := t.TempDir()
	legacyDir := filepath.Join(workDir, legacyNotesDirName)
	require.NoError(t, os.MkdirAll(legacyDir, 0o755))
	require.NoError(t, os.WriteFile(
		filepath.Join(legacyDir, "checkpoints.md"),
		[]byte("# Legacy Checkpoints\n\n- 2026-04-20T00:00:00Z [legacy] Prior note\n"),
		0o644,
	))

	require.NoError(t, RecordRecoveryCheckpoint(workDir, "reviewer", RecoveryCheckpoint{
		Timestamp: time.Date(2026, time.April, 24, 16, 0, 0, 0, time.UTC),
		Trigger:   "resume",
		Summary:   "Continue from imported legacy checkpoints.",
	}))

	archiveBytes, err := os.ReadFile(filepath.Join(workDir, archiveCheckpointsPath))
	require.NoError(t, err)
	require.Contains(t, string(archiveBytes), "# Legacy Checkpoints")
	require.Contains(t, string(archiveBytes), "- 2026-04-20T00:00:00Z [legacy] Prior note")
	require.Contains(t, string(archiveBytes), "- 2026-04-24T16:00:00Z [resume] Continue from imported legacy checkpoints.")
}

func TestWriteManagedKnowledgeBaseIndexPreservesUserContentAndReplacesGeneratedSection(t *testing.T) {
	t.Parallel()

	workDir := t.TempDir()
	require.NoError(t, InitializeMemoryLayout(workDir, "KB Agent"))
	indexPath := filepath.Join(workDir, kbIndexRelPath)
	require.NoError(t, os.WriteFile(indexPath, []byte("# Agent KB\n\nUser-maintained notes stay here.\n"), 0o644))

	first := []ManagedKnowledgeBaseIndexEntry{{Name: "Runbooks", Description: "Ops docs", SourceName: "AgentTeam", RepoRole: "primary", Path: "kb/managed/kb_1/src_1/repo_1-resolved", Branch: "main"}}
	require.NoError(t, WriteManagedKnowledgeBaseIndex(workDir, first))
	second := []ManagedKnowledgeBaseIndexEntry{{Name: "Architecture", Description: "Design docs", SourceName: "Design", RepoRole: "insights", Path: "kb/managed/kb_2/src_2/repo_2-insights", Branch: "main"}}
	require.NoError(t, WriteManagedKnowledgeBaseIndex(workDir, second))

	data, err := os.ReadFile(indexPath)
	require.NoError(t, err)
	content := string(data)
	require.Contains(t, content, "User-maintained notes stay here.")
	require.Contains(t, content, "Architecture")
	require.NotContains(t, content, "Runbooks")
	require.Equal(t, 1, strings.Count(content, "## Managed Knowledge Bases"), "generated heading should be replaced, not duplicated")
	require.Equal(t, 1, strings.Count(content, managedKBIndexStartMarker))
	require.Equal(t, 1, strings.Count(content, managedKBIndexEndMarker))
}

func readJSONFile(path string, target any) error {
	data, err := os.ReadFile(path)
	if err != nil {
		return err
	}
	return json.Unmarshal(data, target)
}
