package persistence

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

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/kits/agentteam-insight/internal/graphbuilder/schema"
)

func TestResolveLayoutDefaultsToGraphDirectoryAndWorktreeRedirect(t *testing.T) {
	input := t.TempDir()
	layout, err := ResolveLayout(input, "")
	require.NoError(t, err)
	require.Equal(t, input, layout.ProjectRoot)
	require.Equal(t, filepath.Join(input, ".agentteam-insight", "knowledge-graph.json"), layout.GraphPath)

	mainRoot := filepath.Join(t.TempDir(), "repo")
	worktreeRoot := filepath.Join(t.TempDir(), "repo-worktree")
	redirected, ok := RedirectWorktreeRootFromGitDirs(worktreeRoot, filepath.Join(mainRoot, ".git", "worktrees", "repo-worktree"), filepath.Join(mainRoot, ".git"))
	require.True(t, ok)
	require.Equal(t, mainRoot, redirected)

	_, ok = RedirectWorktreeRootFromGitDirs(mainRoot, filepath.Join(mainRoot, ".git"), filepath.Join(mainRoot, ".git"))
	require.False(t, ok)
}

func TestSanitizeFilePathDoesNotPersistAbsoluteLocalPaths(t *testing.T) {
	root := t.TempDir()
	require.Equal(t, "src/main.go", SanitizeFilePath(root, filepath.Join(root, "src", "main.go")))
	require.Equal(t, "secret.go", SanitizeFilePath(root, filepath.Join(t.TempDir(), "secret.go")))
	require.Equal(t, "src/main.go", SanitizeFilePath(root, "src/main.go"))
	require.Equal(t, "passwd", SanitizeFilePath(root, "../etc/passwd"))
}

func TestSaveArtifactsWriterOrderWithRecorder(t *testing.T) {
	layout := Layout{
		InputRoot:        "/repo",
		ProjectRoot:      "/repo",
		GraphRoot:        "/repo/.agentteam-insight",
		IntermediateRoot: "/repo/.agentteam-insight/intermediate",
		GraphPath:        "/repo/.agentteam-insight/knowledge-graph.json",
		FingerprintsPath: "/repo/.agentteam-insight/fingerprints.json",
		MetaPath:         "/repo/.agentteam-insight/meta.json",
		ConfigPath:       "/repo/.agentteam-insight/config.json",
	}
	fs := &recordingFS{readDirErr: os.ErrNotExist}
	store := NewStoreWithLayout(layout)
	store.fs = fs

	require.NoError(t, store.SaveArtifacts(sampleArtifacts("/repo")))
	require.Equal(t, []string{
		layout.GraphPath,
		layout.FingerprintsPath,
		layout.MetaPath,
		layout.ConfigPath,
	}, fs.writes)
}

func TestSaveArtifactsValidatesCompanionsBeforeWritingGraph(t *testing.T) {
	layout := Layout{
		InputRoot:        "/repo",
		ProjectRoot:      "/repo",
		GraphRoot:        "/repo/.agentteam-insight",
		IntermediateRoot: "/repo/.agentteam-insight/intermediate",
		GraphPath:        "/repo/.agentteam-insight/knowledge-graph.json",
		FingerprintsPath: "/repo/.agentteam-insight/fingerprints.json",
		MetaPath:         "/repo/.agentteam-insight/meta.json",
		ConfigPath:       "/repo/.agentteam-insight/config.json",
	}
	fs := &recordingFS{readDirErr: os.ErrNotExist}
	store := NewStoreWithLayout(layout)
	store.fs = fs
	artifacts := sampleArtifacts("/repo")
	artifacts.Fingerprints.Version = ""

	require.Error(t, store.SaveArtifacts(artifacts))
	require.Empty(t, fs.writes)
}

func TestSaveArtifactsWritesGraphArtifactsInRequiredOrderAndCleansIntermediate(t *testing.T) {
	input := t.TempDir()
	layout, err := ResolveLayout(input, input)
	require.NoError(t, err)
	require.NoError(t, os.MkdirAll(layout.IntermediateRoot, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(layout.IntermediateRoot, ScanResultFile), []byte(`{"keep":true}`), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(layout.IntermediateRoot, ReviewFile), []byte(`{"keep":true}`), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(layout.IntermediateRoot, "batch-0.json"), []byte(`{"move":true}`), 0o644))
	require.NoError(t, os.MkdirAll(layout.TmpRoot, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(layout.TmpRoot, "agent-task.json"), []byte(`{"move":true}`), 0o644))

	store := NewStoreWithLayout(layout)
	store.now = func() time.Time { return time.Date(2026, 7, 2, 0, 0, 0, 0, time.UTC) }
	artifacts := sampleArtifacts(input)
	require.NoError(t, store.SaveArtifacts(artifacts))

	require.FileExists(t, layout.GraphPath)
	require.FileExists(t, layout.FingerprintsPath)
	require.FileExists(t, layout.MetaPath)
	require.FileExists(t, layout.ConfigPath)
	require.NoFileExists(t, layout.DiffOverlayPath)
	require.NoFileExists(t, layout.DomainGraphPath)
	require.FileExists(t, filepath.Join(layout.IntermediateRoot, ScanResultFile))
	require.FileExists(t, filepath.Join(layout.IntermediateRoot, ReviewFile))
	require.NoFileExists(t, filepath.Join(layout.IntermediateRoot, "batch-0.json"))
	require.NoDirExists(t, layout.TmpRoot)
	require.FileExists(t, filepath.Join(layout.GraphRoot, ".trash-20260702T000000Z", "batch-0.json"))
	require.FileExists(t, filepath.Join(layout.GraphRoot, ".trash-20260702T000000Z", TmpName, "agent-task.json"))

	graphData, err := os.ReadFile(layout.GraphPath)
	require.NoError(t, err)
	require.NotContains(t, string(graphData), input)
	require.Contains(t, string(graphData), "src/main.go")

	infoGraph, err := os.Stat(layout.GraphPath)
	require.NoError(t, err)
	infoFP, err := os.Stat(layout.FingerprintsPath)
	require.NoError(t, err)
	infoMeta, err := os.Stat(layout.MetaPath)
	require.NoError(t, err)
	infoConfig, err := os.Stat(layout.ConfigPath)
	require.NoError(t, err)
	require.False(t, infoFP.ModTime().Before(infoGraph.ModTime()))
	require.False(t, infoMeta.ModTime().Before(infoFP.ModTime()))
	require.False(t, infoConfig.ModTime().Before(infoMeta.ModTime()))
}

func TestSaveArtifactsMovesTmpEvenWhenOnlyScanResultIsPreserved(t *testing.T) {
	input := t.TempDir()
	layout, err := ResolveLayout(input, input)
	require.NoError(t, err)
	require.NoError(t, os.MkdirAll(layout.IntermediateRoot, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(layout.IntermediateRoot, ScanResultFile), []byte(`{"keep":true}`), 0o644))
	require.NoError(t, os.MkdirAll(layout.TmpRoot, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(layout.TmpRoot, "scratch.json"), []byte(`{"move":true}`), 0o644))

	store := NewStoreWithLayout(layout)
	store.now = func() time.Time { return time.Date(2026, 7, 2, 1, 2, 3, 0, time.UTC) }
	require.NoError(t, store.SaveArtifacts(sampleArtifacts(input)))

	require.FileExists(t, filepath.Join(layout.IntermediateRoot, ScanResultFile))
	require.NoDirExists(t, layout.TmpRoot)
	require.FileExists(t, filepath.Join(layout.GraphRoot, ".trash-20260702T010203Z", TmpName, "scratch.json"))
}

func TestSaveArtifactsUsesFreshTrashSlotWhenTimestampCollides(t *testing.T) {
	input := t.TempDir()
	layout, err := ResolveLayout(input, input)
	require.NoError(t, err)
	require.NoError(t, os.MkdirAll(layout.IntermediateRoot, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(layout.IntermediateRoot, ScanResultFile), []byte(`{"keep":true}`), 0o644))
	require.NoError(t, os.MkdirAll(layout.TmpRoot, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(layout.TmpRoot, "first.json"), []byte(`{"move":true}`), 0o644))

	store := NewStoreWithLayout(layout)
	store.now = func() time.Time { return time.Date(2026, 7, 2, 1, 2, 3, 0, time.UTC) }
	require.NoError(t, store.SaveArtifacts(sampleArtifacts(input)))

	require.NoError(t, store.PrepareRun())
	require.NoError(t, os.WriteFile(filepath.Join(layout.TmpRoot, "second.json"), []byte(`{"move":true}`), 0o644))
	require.NoError(t, store.SaveArtifacts(sampleArtifacts(input)))

	require.FileExists(t, filepath.Join(layout.GraphRoot, ".trash-20260702T010203Z", TmpName, "first.json"))
	require.FileExists(t, filepath.Join(layout.GraphRoot, ".trash-20260702T010203Z-1", TmpName, "second.json"))
	require.NoDirExists(t, layout.TmpRoot)
}

func TestSaveArtifactsValidatesGraphBeforeWriting(t *testing.T) {
	input := t.TempDir()
	layout, err := ResolveLayout(input, input)
	require.NoError(t, err)
	store := NewStoreWithLayout(layout)
	artifacts := sampleArtifacts(input)
	artifacts.Graph.Nodes = nil

	require.Error(t, store.SaveArtifacts(artifacts))
	require.NoFileExists(t, layout.GraphPath)
	require.NoFileExists(t, layout.FingerprintsPath)
	require.NoFileExists(t, layout.MetaPath)
}

func TestSaveDiffOverlayIsExplicitAndValidated(t *testing.T) {
	input := t.TempDir()
	store, err := NewStore(input, input)
	require.NoError(t, err)
	require.NoFileExists(t, store.Layout().DiffOverlayPath)

	require.Error(t, store.SaveDiffOverlay(schema.DiffOverlay{ChangedNodeIDs: []string{"a"}}))
	require.NoError(t, store.SaveDiffOverlay(schema.DiffOverlay{ChangedNodeIDs: []string{"a"}, AffectedNodeIDs: []string{"b"}}))
	require.FileExists(t, store.Layout().DiffOverlayPath)
}

func TestLoadConfigDefaultsWhenMissingOrCorrupt(t *testing.T) {
	input := t.TempDir()
	store, err := NewStore(input, input)
	require.NoError(t, err)
	config, err := store.LoadConfig()
	require.NoError(t, err)
	require.Equal(t, schema.ProjectConfig{AutoUpdate: false, OutputLanguage: "en"}, config)

	require.NoError(t, os.MkdirAll(store.Layout().GraphRoot, 0o755))
	require.NoError(t, os.WriteFile(store.Layout().ConfigPath, []byte(`not-json`), 0o644))
	config, err = store.LoadConfig()
	require.NoError(t, err)
	require.Equal(t, schema.ProjectConfig{AutoUpdate: false, OutputLanguage: "en"}, config)
}

func TestGraphEdgeUnknownFieldsAreDroppedWhenPersisted(t *testing.T) {
	input := t.TempDir()
	store, err := NewStore(input, input)
	require.NoError(t, err)
	artifacts := sampleArtifacts(input)
	require.NoError(t, store.SaveArtifacts(artifacts))

	data, err := os.ReadFile(store.Layout().GraphPath)
	require.NoError(t, err)
	var raw map[string]any
	require.NoError(t, json.Unmarshal(data, &raw))
	edge := raw["edges"].([]any)[0].(map[string]any)
	require.NotContains(t, edge, "extra")
}

func sampleArtifacts(inputRoot string) Artifacts {
	return Artifacts{
		Graph: schema.KnowledgeGraph{
			Version: "1.0.0",
			Kind:    schema.KindCodebase,
			Project: schema.ProjectMeta{
				Name:          "project",
				Languages:     []string{"go"},
				Frameworks:    []string{},
				Description:   "project",
				AnalyzedAt:    "2026-07-02T00:00:00Z",
				GitCommitHash: "abc123",
			},
			Nodes: []schema.GraphNode{{
				ID:         "file:src/main.go",
				Type:       "file",
				Name:       "main.go",
				FilePath:   filepath.Join(inputRoot, "src", "main.go"),
				Summary:    "Main",
				Tags:       []string{},
				Complexity: "moderate",
			}},
			Edges:  []schema.GraphEdge{{Source: "file:src/main.go", Target: "file:src/main.go", Type: "imports", Direction: "forward", Weight: 0.5}},
			Layers: []schema.Layer{},
			Tour:   []schema.TourStep{},
		},
		Fingerprints: schema.FingerprintStore{
			Version:       "1.0.0",
			GitCommitHash: "abc123",
			GeneratedAt:   "2026-07-02T00:00:00Z",
			Files: map[string]schema.FileFingerprint{
				"src/main.go": {FilePath: "src/main.go", ContentHash: "hash", TotalLines: 1, HasStructuralAnalysis: false},
			},
		},
		Meta: schema.AnalysisMeta{
			LastAnalyzedAt: "2026-07-02T00:00:00Z",
			GitCommitHash:  "abc123",
			Version:        "1.0.0",
			AnalyzedFiles:  1,
		},
		Config:      schema.ProjectConfig{AutoUpdate: false, OutputLanguage: "en"},
		WriteConfig: true,
	}
}

type recordingFS struct {
	writes     []string
	readDirErr error
}

func (fsys *recordingFS) MkdirAll(path string, perm os.FileMode) error { return nil }

func (fsys *recordingFS) WriteJSON(path string, value any) error {
	_, err := json.Marshal(value)
	if err != nil {
		return err
	}
	fsys.writes = append(fsys.writes, path)
	return nil
}

func (fsys *recordingFS) ReadFile(path string) ([]byte, error) { return nil, os.ErrNotExist }

func (fsys *recordingFS) Rename(oldpath string, newpath string) error { return nil }

func (fsys *recordingFS) ReadDir(path string) ([]os.DirEntry, error) {
	if fsys.readDirErr != nil {
		return nil, fsys.readDirErr
	}
	return nil, nil
}

func (fsys *recordingFS) RemoveAll(path string) error { return nil }

func TestPrepareRunCreatesDirectoriesAndPurgesStaleTrash(t *testing.T) {
	input := t.TempDir()
	layout, err := ResolveLayout(input, input)
	require.NoError(t, err)
	require.NoError(t, os.MkdirAll(filepath.Join(layout.GraphRoot, ".trash-old"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(layout.GraphRoot, ".trash-old", "batch.json"), []byte(`{}`), 0o644))
	require.NoError(t, os.MkdirAll(filepath.Join(layout.GraphRoot, ".trash-new"), 0o755))
	old := time.Date(2026, 6, 20, 0, 0, 0, 0, time.UTC)
	newer := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
	require.NoError(t, os.Chtimes(filepath.Join(layout.GraphRoot, ".trash-old"), old, old))
	require.NoError(t, os.Chtimes(filepath.Join(layout.GraphRoot, ".trash-new"), newer, newer))

	store := NewStoreWithLayout(layout)
	store.now = func() time.Time { return time.Date(2026, 7, 2, 0, 0, 0, 0, time.UTC) }
	require.NoError(t, store.PrepareRun())

	require.DirExists(t, layout.IntermediateRoot)
	require.DirExists(t, layout.TmpRoot)
	require.NoDirExists(t, filepath.Join(layout.GraphRoot, ".trash-old"))
	require.DirExists(t, filepath.Join(layout.GraphRoot, ".trash-new"))
}

func TestSaveConfigWritesOnlyConfig(t *testing.T) {
	layout := Layout{
		InputRoot:        "/repo",
		ProjectRoot:      "/repo",
		GraphRoot:        "/repo/.agentteam-insight",
		IntermediateRoot: "/repo/.agentteam-insight/intermediate",
		TmpRoot:          "/repo/.agentteam-insight/tmp",
		GraphPath:        "/repo/.agentteam-insight/knowledge-graph.json",
		FingerprintsPath: "/repo/.agentteam-insight/fingerprints.json",
		MetaPath:         "/repo/.agentteam-insight/meta.json",
		ConfigPath:       "/repo/.agentteam-insight/config.json",
	}
	fs := &recordingFS{readDirErr: os.ErrNotExist}
	store := NewStoreWithLayout(layout)
	store.fs = fs

	require.NoError(t, store.SaveConfig(schema.ProjectConfig{AutoUpdate: true, OutputLanguage: "zh"}))
	require.Equal(t, []string{layout.ConfigPath}, fs.writes)
}
