package shared

import (
	"io/fs"
	"reflect"
	"strings"
	"testing"
	"testing/fstest"

	"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/promptfs"
)

func TestPromptAssetManifestPromptFS(t *testing.T) {
	manifest := PromptAssetManifest{
		ModuleDir: "dnd.test",
		ModuleFiles: []promptfs.ModulePromptFile{
			{Name: "prompt.yaml", Path: "assets/prompts/prompt.yaml"},
			{Name: "instructions.md", Path: "assets/prompts/instructions.md"},
		},
		SharedFiles: []string{
			"common-dnd-system.md",
			"common-dnd-transcript-full.md",
			"common-dnd-transcript-chunk.md",
		},
	}

	fsys, err := manifest.PromptFS(fstest.MapFS{
		"assets/prompts/prompt.yaml":     {Data: []byte("id: dnd.test")},
		"assets/prompts/instructions.md": {Data: []byte("instructions")},
	})
	if err != nil {
		t.Fatalf("PromptFS() error = %v, want nil", err)
	}

	wantModule := map[string]string{
		"assets/prompts/dnd.test/prompt.yaml":     "id: dnd.test",
		"assets/prompts/dnd.test/instructions.md": "instructions",
	}
	for path, wantContent := range wantModule {
		content, err := fs.ReadFile(fsys, path)
		if err != nil {
			t.Fatalf("ReadFile(%q) error = %v, want nil", path, err)
		}
		if string(content) != wantContent {
			t.Fatalf("ReadFile(%q) = %q, want %q", path, content, wantContent)
		}
	}
	for _, path := range []string{
		"assets/prompts/dnd.test/sharedassets/common-dnd-system.md",
		"assets/prompts/dnd.test/sharedassets/common-dnd-transcript-full.md",
		"assets/prompts/dnd.test/sharedassets/common-dnd-transcript-chunk.md",
	} {
		content, err := fs.ReadFile(fsys, path)
		if err != nil {
			t.Fatalf("ReadFile(%q) error = %v, want nil", path, err)
		}
		if len(content) == 0 {
			t.Fatalf("ReadFile(%q) returned empty content, want mounted asset", path)
		}
	}
	for _, path := range []string{
		"assets/prompts/dnd.test/sharedassets/common-dnd-references.md",
		"assets/prompts/dnd.test/sharedassets/common-dnd-npc-registry.md",
	} {
		if _, err := fs.ReadFile(fsys, path); err == nil {
			t.Fatalf("ReadFile(%q) succeeded, want unlisted shared asset to be absent", path)
		}
	}
}

func TestPromptAssetManifestRejectsInvalidSharedNames(t *testing.T) {
	tests := []struct {
		name        string
		sharedFiles []string
		wantError   string
	}{
		{name: "unknown", sharedFiles: []string{"missing.md"}, wantError: "unknown shared prompt asset name"},
		{name: "duplicate", sharedFiles: []string{"common-dnd-system.md", "common-dnd-system.md"}, wantError: "duplicate shared prompt asset name"},
		{name: "empty", sharedFiles: []string{""}, wantError: "must not be empty"},
		{name: "slash path", sharedFiles: []string{"nested/common-dnd-system.md"}, wantError: "must not contain path separators"},
		{name: "backslash path", sharedFiles: []string{`nested\common-dnd-system.md`}, wantError: "must not contain path separators"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			_, err := (PromptAssetManifest{
				ModuleDir: "dnd.test",
				ModuleFiles: []promptfs.ModulePromptFile{
					{Name: "prompt.yaml", Path: "assets/prompts/prompt.yaml"},
				},
				SharedFiles: test.sharedFiles,
			}).PromptFS(fstest.MapFS{
				"assets/prompts/prompt.yaml": {Data: []byte("id: dnd.test")},
			})
			if err == nil || !strings.Contains(err.Error(), test.wantError) {
				t.Fatalf("PromptFS() error = %v, want %q", err, test.wantError)
			}
		})
	}
}

func TestPromptAssetManifestRejectsMissingModuleFile(t *testing.T) {
	_, err := (PromptAssetManifest{
		ModuleDir: "dnd.test",
		ModuleFiles: []promptfs.ModulePromptFile{
			{Name: "instructions.md", Path: "assets/prompts/instructions.md"},
		},
	}).PromptFS(fstest.MapFS{})
	if err == nil || !strings.Contains(err.Error(), "read module prompt asset assets/prompts/instructions.md") {
		t.Fatalf("PromptFS() error = %v, want missing module asset context", err)
	}
}

func TestPromptAssetManifestRejectsMissingSharedFile(t *testing.T) {
	const name = "missing-for-test.md"
	const path = "prompts/missing-for-test.md"
	previous, existed := sharedPromptPaths[name]
	sharedPromptPaths[name] = path
	t.Cleanup(func() {
		if existed {
			sharedPromptPaths[name] = previous
		} else {
			delete(sharedPromptPaths, name)
		}
	})

	_, err := (PromptAssetManifest{
		ModuleDir: "dnd.test",
		ModuleFiles: []promptfs.ModulePromptFile{
			{Name: "prompt.yaml", Path: "assets/prompts/prompt.yaml"},
		},
		SharedFiles: []string{name},
	}).PromptFS(fstest.MapFS{
		"assets/prompts/prompt.yaml": {Data: []byte("id: dnd.test")},
	})
	if err == nil || !strings.Contains(err.Error(), "read shared prompt asset "+path) {
		t.Fatalf("PromptFS() error = %v, want missing shared asset context", err)
	}
}

func TestPromptAssetManifestHashMatchesManifestParts(t *testing.T) {
	moduleFS := fstest.MapFS{
		"assets/prompts/prompt.yaml":     {Data: []byte("id: dnd.test")},
		"assets/prompts/instructions.md": {Data: []byte("instructions")},
	}
	manifest := PromptAssetManifest{
		ModuleDir: "dnd.test",
		ModuleFiles: []promptfs.ModulePromptFile{
			{Name: "prompt.yaml", Path: "assets/prompts/prompt.yaml"},
			{Name: "instructions.md", Path: "assets/prompts/instructions.md"},
		},
		SharedFiles: []string{
			"common-dnd-transcript-chunk.md",
			"common-dnd-system.md",
		},
	}

	got, err := manifest.Hash(moduleFS)
	if err != nil {
		t.Fatalf("Hash() error = %v, want nil", err)
	}
	sharedFS, err := sharedAssetFS()
	if err != nil {
		t.Fatalf("sharedAssetFS() error = %v, want nil", err)
	}
	want, err := llm.HashAssets([]llm.AssetHashPart{
		{FS: moduleFS, Path: "assets/prompts/prompt.yaml"},
		{FS: moduleFS, Path: "assets/prompts/instructions.md"},
		{FS: sharedFS, Path: "prompts/common-dnd-transcript-chunk.md"},
		{FS: sharedFS, Path: "prompts/common-dnd-system.md"},
	})
	if err != nil {
		t.Fatalf("HashAssets() error = %v, want nil", err)
	}
	if got != want {
		t.Fatalf("Hash() = %q, want independently assembled manifest hash %q", got, want)
	}
	withUnused, err := llm.HashAssets([]llm.AssetHashPart{
		{FS: moduleFS, Path: "assets/prompts/prompt.yaml"},
		{FS: moduleFS, Path: "assets/prompts/instructions.md"},
		{FS: sharedFS, Path: "prompts/common-dnd-transcript-chunk.md"},
		{FS: sharedFS, Path: "prompts/common-dnd-system.md"},
		{FS: sharedFS, Path: "prompts/common-dnd-npc-registry.md"},
	})
	if err != nil {
		t.Fatalf("HashAssets() with unused asset error = %v, want nil", err)
	}
	if got == withUnused {
		t.Fatalf("Hash() included an unlisted shared asset")
	}
}

func TestSharedPromptDescriptorsReturnFreshCopies(t *testing.T) {
	first, err := resolveSharedPromptFiles([]string{"common-dnd-system.md"})
	if err != nil {
		t.Fatalf("resolveSharedPromptFiles() error = %v, want nil", err)
	}
	second, err := resolveSharedPromptFiles([]string{"common-dnd-system.md"})
	if err != nil {
		t.Fatalf("resolveSharedPromptFiles() second error = %v, want nil", err)
	}
	first[0].Name = "changed.md"
	first[0].Path = "changed.md"
	if reflect.DeepEqual(first, second) || second[0].Name != "common-dnd-system.md" || second[0].Path != "prompts/common-dnd-system.md" {
		t.Fatalf("resolveSharedPromptFiles() reused descriptor state: first=%#v second=%#v", first, second)
	}
}

func TestPromptAssetManifestMountsExternalSharedFiles(t *testing.T) {
	external := fstest.MapFS{"core/protocol.md": {Data: []byte("integer protocol")}}
	manifest := PromptAssetManifest{
		ModuleDir: "dnd.test",
		ModuleFiles: []promptfs.ModulePromptFile{
			{Name: "prompt.yaml", Path: "prompts/prompt.yaml"},
		},
		ExternalSharedFiles: []promptfs.SharedPromptFile{
			{Name: "protocol.md", FS: external, Path: "core/protocol.md"},
		},
	}
	fys, err := manifest.PromptFS(fstest.MapFS{
		"prompts/prompt.yaml": {Data: []byte("id: dnd.test")},
	})
	if err != nil {
		t.Fatalf("PromptFS() error = %v, want nil", err)
	}
	content, err := fs.ReadFile(fys, "assets/prompts/dnd.test/sharedassets/protocol.md")
	if err != nil || string(content) != "integer protocol" {
		t.Fatalf("mounted external protocol = %q, %v", content, err)
	}
}

func TestPromptAssetManifestHashIncludesExternalSharedFiles(t *testing.T) {
	moduleFS := fstest.MapFS{"prompts/prompt.yaml": {Data: []byte("id: dnd.test")}}
	external := fstest.MapFS{"core/protocol.md": {Data: []byte("integer protocol")}}
	manifest := PromptAssetManifest{
		ModuleDir: "dnd.test",
		ModuleFiles: []promptfs.ModulePromptFile{
			{Name: "prompt.yaml", Path: "prompts/prompt.yaml"},
		},
		ExternalSharedFiles: []promptfs.SharedPromptFile{
			{Name: "protocol.md", FS: external, Path: "core/protocol.md"},
		},
	}
	got, err := manifest.Hash(moduleFS)
	if err != nil {
		t.Fatalf("Hash() error = %v, want nil", err)
	}
	want, err := llm.HashAssets([]llm.AssetHashPart{
		{FS: moduleFS, Path: "prompts/prompt.yaml"},
		{FS: external, Path: "core/protocol.md"},
	})
	if err != nil {
		t.Fatal(err)
	}
	if got != want {
		t.Fatalf("Hash() = %q, want external-aware hash %q", got, want)
	}
}

func TestPromptAssetManifestRejectsInvalidExternalSharedFiles(t *testing.T) {
	moduleFS := fstest.MapFS{"prompts/prompt.yaml": {Data: []byte("id: dnd.test")}}
	tests := []struct {
		name     string
		external []promptfs.SharedPromptFile
		shared   []string
		want     string
	}{
		{
			name: "missing",
			external: []promptfs.SharedPromptFile{
				{Name: "protocol.md", FS: fstest.MapFS{}, Path: "core/protocol.md"},
			},
			want: "read shared prompt asset core/protocol.md",
		},
		{
			name: "duplicate external destinations",
			external: []promptfs.SharedPromptFile{
				{Name: "protocol.md", FS: fstest.MapFS{"a.md": {Data: []byte("a")}}, Path: "a.md"},
				{Name: " protocol.md ", FS: fstest.MapFS{"b.md": {Data: []byte("b")}}, Path: "b.md"},
			},
			want: `duplicate sharedassets prompt destination "assets/prompts/dnd.test/sharedassets/protocol.md"`,
		},
		{
			name:   "duplicate named and external destinations",
			shared: []string{"common-dnd-system.md"},
			external: []promptfs.SharedPromptFile{
				{Name: "common-dnd-system.md", FS: fstest.MapFS{"system.md": {Data: []byte("external")}}, Path: "system.md"},
			},
			want: `duplicate sharedassets prompt destination "assets/prompts/dnd.test/sharedassets/common-dnd-system.md"`,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			manifest := PromptAssetManifest{
				ModuleDir: "dnd.test",
				ModuleFiles: []promptfs.ModulePromptFile{
					{Name: "prompt.yaml", Path: "prompts/prompt.yaml"},
				},
				SharedFiles:         test.shared,
				ExternalSharedFiles: test.external,
			}
			if _, err := manifest.PromptFS(moduleFS); err == nil || !strings.Contains(err.Error(), test.want) {
				t.Fatalf("PromptFS() error = %v, want %q", err, test.want)
			}
			if _, err := manifest.Hash(moduleFS); err == nil || !strings.Contains(err.Error(), test.want) {
				t.Fatalf("Hash() error = %v, want %q", err, test.want)
			}
		})
	}
}
