package workspace

import (
	"context"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"chat-switchboard/models"
)

// ── Path Cleaning Tests ─────────────────────

func TestCleanPath(t *testing.T) {
	tests := []struct {
		input string
		want  string
	}{
		{"src/main.go", "src/main.go"},
		{"/src/main.go", "src/main.go"},
		{"./src/main.go", "src/main.go"},
		{"src/../etc/passwd", "etc/passwd"},
		{"../etc/passwd", ""},
		{"../..", ""},
		{"..", ""},
		{".", ""},
		{"", ""},
		{"  src/main.go  ", "src/main.go"},
		{"src/./main.go", "src/main.go"},
		{".gitignore", ".gitignore"},
		{".env", ".env"},
		{"src/.hidden/file.go", "src/.hidden/file.go"},
	}

	for _, tt := range tests {
		t.Run(tt.input, func(t *testing.T) {
			got := cleanPath(tt.input)
			if got != tt.want {
				t.Errorf("cleanPath(%q) = %q, want %q", tt.input, got, tt.want)
			}
		})
	}
}

func TestAbsPathTraversal(t *testing.T) {
	tmpDir := t.TempDir()
	fs := &FS{basePath: tmpDir}

	w := &models.Workspace{
		BaseModel: models.BaseModel{ID: "test-workspace"},
	}

	// Create the files dir so absPath has a real root to resolve against
	filesDir := filepath.Join(tmpDir, w.ID, filesSubdir)
	os.MkdirAll(filesDir, 0750)

	// Valid paths should work
	validPaths := []string{
		"src/main.go",
		"README.md",
		".gitignore",
		"deeply/nested/path/file.txt",
	}
	for _, p := range validPaths {
		abs, err := fs.absPath(w, p)
		if err != nil {
			t.Errorf("absPath(%q) unexpected error: %v", p, err)
			continue
		}
		if !strings.HasPrefix(abs, filesDir) {
			t.Errorf("absPath(%q) = %s, not under %s", p, abs, filesDir)
		}
	}

	// Traversal attempts should fail
	traversalPaths := []string{
		"../../../etc/passwd",
		"src/../../etc/passwd",
	}
	for _, p := range traversalPaths {
		_, err := fs.absPath(w, p)
		if err == nil {
			t.Errorf("absPath(%q) should have returned error for traversal", p)
		}
	}
}

// ── Content Type Detection Tests ────────────

func TestMimeByExtension(t *testing.T) {
	tests := []struct {
		ext  string
		want string
	}{
		{".go", "text/x-go"},
		{".py", "text/x-python"},
		{".rs", "text/x-rust"},
		{".ts", "text/typescript"},
		{".sh", "text/x-shellscript"},
		{".sql", "text/x-sql"},
		{".yaml", "text/yaml"},
		{".yml", "text/yaml"},
		{".toml", "application/toml"},
		{".pl", "text/x-perl"},
		{".pm", "text/x-perl"},
		{".lua", "text/x-lua"},
		{".dockerfile", "text/x-dockerfile"},
		{".unknown", ""},
	}

	for _, tt := range tests {
		t.Run(tt.ext, func(t *testing.T) {
			got := mimeByExtension(tt.ext)
			if got != tt.want {
				t.Errorf("mimeByExtension(%q) = %q, want %q", tt.ext, got, tt.want)
			}
		})
	}
}

// ── Unsafe Path Tests ───────────────────────

func TestIsUnsafePath(t *testing.T) {
	tests := []struct {
		path   string
		unsafe bool
	}{
		{"src/main.go", false},
		{".gitignore", false},
		{".env", false},
		{"../etc/passwd", true},
		{"/etc/passwd", true},
		{"src/../../../etc/passwd", true},
		{"\\windows\\system32", true},
	}

	for _, tt := range tests {
		t.Run(tt.path, func(t *testing.T) {
			got := isUnsafePath(tt.path)
			if got != tt.unsafe {
				t.Errorf("isUnsafePath(%q) = %v, want %v", tt.path, got, tt.unsafe)
			}
		})
	}
}

// ── Common Prefix Detection Tests ───────────

func TestDetectCommonPrefix(t *testing.T) {
	tests := []struct {
		name  string
		names []string
		want  string
	}{
		{
			name:  "common prefix",
			names: []string{"project/src/a.go", "project/src/b.go", "project/README.md"},
			want:  "project/",
		},
		{
			name:  "no common prefix",
			names: []string{"src/a.go", "lib/b.go"},
			want:  "",
		},
		{
			name:  "file at root",
			names: []string{"project/src/a.go", "Makefile"},
			want:  "",
		},
		{
			name:  "empty",
			names: []string{},
			want:  "",
		},
		{
			name:  "single dir",
			names: []string{"myproject/file.txt"},
			want:  "myproject/",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := detectCommonPrefix(tt.names)
			if got != tt.want {
				t.Errorf("detectCommonPrefix(%v) = %q, want %q", tt.names, got, tt.want)
			}
		})
	}
}

// ── Integration: Write + Read Round-Trip ────

type mockWorkspaceStore struct {
	files map[string]*models.WorkspaceFile
}

func newMockStore() *mockWorkspaceStore {
	return &mockWorkspaceStore{files: make(map[string]*models.WorkspaceFile)}
}

func (m *mockWorkspaceStore) Create(ctx context.Context, w *models.Workspace) error { return nil }
func (m *mockWorkspaceStore) GetByID(ctx context.Context, id string) (*models.Workspace, error) {
	return nil, nil
}
func (m *mockWorkspaceStore) Update(ctx context.Context, id string, patch models.WorkspacePatch) error {
	return nil
}
func (m *mockWorkspaceStore) Delete(ctx context.Context, id string) error { return nil }
func (m *mockWorkspaceStore) GetByOwner(ctx context.Context, ownerType, ownerID string) (*models.Workspace, error) {
	return nil, nil
}
func (m *mockWorkspaceStore) ListByOwner(ctx context.Context, ownerType, ownerID string) ([]models.Workspace, error) {
	return nil, nil
}
func (m *mockWorkspaceStore) GetStats(ctx context.Context, workspaceID string) (*models.WorkspaceStats, error) {
	return nil, nil
}

func (m *mockWorkspaceStore) UpsertFile(ctx context.Context, f *models.WorkspaceFile) error {
	key := f.WorkspaceID + ":" + f.Path
	m.files[key] = f
	return nil
}
func (m *mockWorkspaceStore) DeleteFile(ctx context.Context, workspaceID, path string) error {
	delete(m.files, workspaceID+":"+path)
	return nil
}
func (m *mockWorkspaceStore) DeleteFilesByPrefix(ctx context.Context, workspaceID, prefix string) error {
	for k := range m.files {
		if strings.HasPrefix(k, workspaceID+":"+prefix) {
			delete(m.files, k)
		}
	}
	return nil
}
func (m *mockWorkspaceStore) GetFile(ctx context.Context, workspaceID, path string) (*models.WorkspaceFile, error) {
	f, ok := m.files[workspaceID+":"+path]
	if !ok {
		return nil, os.ErrNotExist
	}
	return f, nil
}
func (m *mockWorkspaceStore) ListFiles(ctx context.Context, workspaceID, prefix string, recursive bool) ([]models.WorkspaceFile, error) {
	var out []models.WorkspaceFile
	for _, f := range m.files {
		if f.WorkspaceID == workspaceID {
			if prefix == "" || strings.HasPrefix(f.Path, prefix) {
				out = append(out, *f)
			}
		}
	}
	return out, nil
}
func (m *mockWorkspaceStore) DeleteAllFiles(ctx context.Context, workspaceID string) error {
	for k, f := range m.files {
		if f.WorkspaceID == workspaceID {
			delete(m.files, k)
		}
	}
	return nil
}

// Chunk methods (v0.21.2) — no-ops for FS tests
func (m *mockWorkspaceStore) InsertChunks(ctx context.Context, chunks []models.WorkspaceChunk) error {
	return nil
}
func (m *mockWorkspaceStore) DeleteChunksByFile(ctx context.Context, fileID string) error {
	return nil
}
func (m *mockWorkspaceStore) SimilaritySearch(ctx context.Context, workspaceID string, queryVec []float64, threshold float64, limit int) ([]models.WorkspaceChunkResult, error) {
	return nil, nil
}
func (m *mockWorkspaceStore) UpdateFileIndexStatus(ctx context.Context, fileID, status string, chunkCount int) error {
	return nil
}
func (m *mockWorkspaceStore) SetGitLastSync(ctx context.Context, workspaceID string, t time.Time) error {
	return nil
}

func TestWriteReadRoundTrip(t *testing.T) {
	tmpDir := t.TempDir()
	mock := newMockStore()
	wfs := NewFS(tmpDir, mock)

	w := &models.Workspace{
		BaseModel: models.BaseModel{ID: "test-ws"},
		Status:    "active",
	}
	if err := wfs.CreateDir(w); err != nil {
		t.Fatalf("CreateDir: %v", err)
	}

	ctx := context.Background()
	content := "package main\n\nfunc main() {}\n"

	// Write
	err := wfs.WriteFile(ctx, w, "src/main.go", strings.NewReader(content), int64(len(content)))
	if err != nil {
		t.Fatalf("WriteFile: %v", err)
	}

	// Verify DB index was updated
	f, ok := mock.files["test-ws:src/main.go"]
	if !ok {
		t.Fatal("expected file in mock store index")
	}
	if f.ContentType != "text/x-go" {
		t.Errorf("content_type = %q, want %q", f.ContentType, "text/x-go")
	}
	if f.SizeBytes != int64(len(content)) {
		t.Errorf("size_bytes = %d, want %d", f.SizeBytes, len(content))
	}
	if f.SHA256 == "" {
		t.Error("expected non-empty sha256")
	}

	// Read back
	rc, size, err := wfs.ReadFile(ctx, w, "src/main.go")
	if err != nil {
		t.Fatalf("ReadFile: %v", err)
	}
	defer rc.Close()

	if size != int64(len(content)) {
		t.Errorf("read size = %d, want %d", size, len(content))
	}

	buf := make([]byte, size)
	if _, err := rc.Read(buf); err != nil {
		t.Fatalf("Read: %v", err)
	}
	if string(buf) != content {
		t.Errorf("content = %q, want %q", string(buf), content)
	}
}

func TestDeleteFile(t *testing.T) {
	tmpDir := t.TempDir()
	mock := newMockStore()
	wfs := NewFS(tmpDir, mock)

	w := &models.Workspace{
		BaseModel: models.BaseModel{ID: "test-ws"},
	}
	wfs.CreateDir(w)

	ctx := context.Background()
	content := "hello"
	wfs.WriteFile(ctx, w, "test.txt", strings.NewReader(content), int64(len(content)))

	// Delete
	err := wfs.DeleteFile(ctx, w, "test.txt", false)
	if err != nil {
		t.Fatalf("DeleteFile: %v", err)
	}

	// Verify removed from index
	if _, ok := mock.files["test-ws:test.txt"]; ok {
		t.Error("expected file removed from index")
	}

	// Verify removed from filesystem
	abs := filepath.Join(tmpDir, w.ID, filesSubdir, "test.txt")
	if _, err := os.Stat(abs); !os.IsNotExist(err) {
		t.Error("expected file removed from filesystem")
	}
}

func TestMkdir(t *testing.T) {
	tmpDir := t.TempDir()
	mock := newMockStore()
	wfs := NewFS(tmpDir, mock)

	w := &models.Workspace{
		BaseModel: models.BaseModel{ID: "test-ws"},
	}
	wfs.CreateDir(w)

	ctx := context.Background()
	err := wfs.Mkdir(ctx, w, "src/pkg/handlers")
	if err != nil {
		t.Fatalf("Mkdir: %v", err)
	}

	// Verify on disk
	abs := filepath.Join(tmpDir, w.ID, filesSubdir, "src/pkg/handlers")
	info, err := os.Stat(abs)
	if err != nil {
		t.Fatalf("stat: %v", err)
	}
	if !info.IsDir() {
		t.Error("expected directory")
	}

	// Verify in index
	f, ok := mock.files["test-ws:src/pkg/handlers"]
	if !ok {
		t.Fatal("expected dir in mock store")
	}
	if !f.IsDirectory {
		t.Error("expected is_directory = true")
	}
}
