package automation

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/agsbridge/wiki"
	"github.com/tidbcloud/AgentTeam/internal/service/automation/runtime"
)

func TestAGSAutomationWikiPublisherPublishesMarkdownPages(t *testing.T) {
	root := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(root, "README.md"), []byte("# Home\n"), 0o644))
	require.NoError(t, os.MkdirAll(filepath.Join(root, "ops"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(root, "ops", "deploy.md"), []byte("# Deploy\n"), 0o644))
	require.NoError(t, os.MkdirAll(filepath.Join(root, ".ignored"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(root, ".ignored", "secret.md"), []byte("# Hidden\n"), 0o644))
	store := &fakeAGSRepositoryStore{}
	publisher := AGSAutomationWikiPublisher{Store: store}

	result, err := publisher.PublishWikiPages(t.Context(), runtime.WikiPublishRequest{
		WorkspaceID:    "ws-1",
		RunID:          "wfr-1",
		Repository:     "pingcap/tidb.wiki",
		SourcePath:     root,
		Description:    "Generated TiDB wiki",
		Label:          "automation-preset-generated",
		MaxPages:       10,
		DefaultBranch:  "main",
		Visibility:     "private",
		RepositoryKind: "automation_preset_wiki",
	})

	require.NoError(t, err)
	require.Equal(t, runtime.WikiPublishResult{Repository: "pingcap/tidb.wiki", Pages: 2, Bytes: int64(len("# Home\n") + len("# Deploy\n")), IndexedCommitSHA: "abc123", Reconciled: true}, result)
	require.Len(t, store.ensureRequests, 1)
	require.Equal(t, "pingcap", store.ensureRequests[0].Owner)
	require.Equal(t, "tidb.wiki", store.ensureRequests[0].Name)
	require.Equal(t, "automation_preset_wiki", store.ensureRequests[0].Kind)
	require.True(t, store.ensureRequests[0].Features.Wiki)
	require.True(t, store.ensureRequests[0].UserVisible)
	require.Len(t, store.writes, 2)
	require.Equal(t, "overview", store.writes[0].Page.Slug)
	require.Equal(t, []string{"automation-preset-generated"}, store.writes[0].Page.Labels)
	require.Equal(t, "ops/deploy", store.writes[1].Page.Slug)
	require.True(t, store.reconciled)
}

func TestAGSAutomationWikiPublisherNormalizesMountedPathsToAGSWritableSlugs(t *testing.T) {
	root := t.TempDir()
	writePage := func(relPath string) {
		t.Helper()
		path := filepath.Join(root, filepath.FromSlash(relPath))
		require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
		require.NoError(t, os.WriteFile(path, []byte("# "+relPath+"\n"), 0o644))
	}
	writePage("copilot_chat/quick_start.md")
	writePage("pkg/statistics/handle/autoanalyze/priorityqueue/calculatoranalysis/README.md")
	writePage("lightning/tests/lightning_duplicate_resolution_replace_one_key_multiple_conflicts_nonclustered_pk/run.sh.md")
	writePage("foo-bar.md")
	writePage("foo_bar.md")
	store := &validatingAGSRepositoryStore{}
	publisher := AGSAutomationWikiPublisher{Store: store, MaxPages: 10, MaxEntries: 20}

	result, err := publisher.PublishWikiPages(t.Context(), runtime.WikiPublishRequest{
		Repository: "pingcap/tidb.insights",
		SourcePath: root,
		MaxPages:   10,
	})

	require.NoError(t, err)
	require.Equal(t, 5, result.Pages)
	slugs := map[string]struct{}{}
	for _, write := range store.writes {
		require.NoError(t, testAGSWikiSlugWritable(write.Page.Slug))
		require.NotContains(t, write.Page.Slug, "_")
		slugs[write.Page.Slug] = struct{}{}
	}
	require.Contains(t, slugs, "copilot-chat/quick-start")
	require.Contains(t, slugs, "pkg/statistics/handle/autoanalyze/priorityqueue/calculatoranalysis-overview")
	require.Contains(t, slugs, "foo-bar")
	require.True(t, testHasSlugPrefix(slugs, "foo-bar-"), "expected colliding foo_bar.md to receive a stable hash suffix")
	require.True(t, testHasSlugPrefix(slugs, "lightning/tests/lightning-duplicate-resolution-replace-one-key-"), "expected long segment to be shortened")
}

func TestAGSAutomationWikiPublisherAvoidsAGSPrefixConflictsForDirectoryReadmes(t *testing.T) {
	root := t.TempDir()
	require.NoError(t, os.MkdirAll(filepath.Join(root, "br"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(root, "br", "README.md"), []byte("# BR\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(root, "br", "COMPATIBILITY_TEST.md"), []byte("# Compatibility\n"), 0o644))
	require.NoError(t, os.MkdirAll(filepath.Join(root, "guide"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(root, "guide.md"), []byte("# Guide\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(root, "guide", "setup.md"), []byte("# Setup\n"), 0o644))
	store := &validatingAGSRepositoryStore{liveSlugs: map[string]struct{}{"br": {}}}
	publisher := AGSAutomationWikiPublisher{Store: store, MaxPages: 10, MaxEntries: 20}

	result, err := publisher.PublishWikiPages(t.Context(), runtime.WikiPublishRequest{
		Repository: "pingcap/tidb.insights",
		SourcePath: root,
		Label:      "automation-preset-generated",
		MaxPages:   10,
	})

	require.NoError(t, err)
	require.Equal(t, 4, result.Pages)
	require.Len(t, store.prunes, 1)
	require.ElementsMatch(t, []string{"br/compatibility-test", "br/overview", "guide", "guide-setup"}, store.prunes[0].KeepSlugs)
	slugs := make([]string, 0, len(store.writes))
	for _, write := range store.writes {
		slugs = append(slugs, write.Page.Slug)
	}
	require.ElementsMatch(t, []string{"br/compatibility-test", "br/overview", "guide", "guide-setup"}, slugs)
}

func TestAGSAutomationWikiPublisherRejectsTooManyPages(t *testing.T) {
	root := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(root, "one.md"), []byte("# One\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(root, "two.md"), []byte("# Two\n"), 0o644))
	publisher := AGSAutomationWikiPublisher{Store: &fakeAGSRepositoryStore{}}

	_, err := publisher.PublishWikiPages(t.Context(), runtime.WikiPublishRequest{
		Repository: "pingcap/tidb.wiki",
		SourcePath: root,
		MaxPages:   1,
	})

	require.Error(t, err)
	require.Contains(t, err.Error(), "exceeds 1 markdown pages")
}

func TestAGSAutomationWikiPublisherPublishesMoreThanLegacyDefaultWhenConfigured(t *testing.T) {
	root := t.TempDir()
	for i := 0; i < 1001; i++ {
		name := filepath.Join(root, "page-"+strconv.Itoa(i)+".md")
		require.NoError(t, os.WriteFile(name, []byte("# Page\n"), 0o644))
	}
	store := &fakeAGSRepositoryStore{}
	publisher := AGSAutomationWikiPublisher{Store: store, MaxPages: 1001, MaxEntries: 1200}

	result, err := publisher.PublishWikiPages(t.Context(), runtime.WikiPublishRequest{
		Repository: "pingcap/tidb.insights",
		SourcePath: root,
		MaxPages:   1001,
	})

	require.NoError(t, err)
	require.Equal(t, 1001, result.Pages)
	require.Len(t, store.writes, 1001)
}

func TestAGSAutomationWikiPublisherRejectsConfiguredEntryLimit(t *testing.T) {
	root := t.TempDir()
	require.NoError(t, os.Mkdir(filepath.Join(root, "one"), 0o755))
	require.NoError(t, os.Mkdir(filepath.Join(root, "two"), 0o755))
	publisher := AGSAutomationWikiPublisher{Store: &fakeAGSRepositoryStore{}, MaxPages: 10, MaxEntries: 2}

	_, err := publisher.PublishWikiPages(t.Context(), runtime.WikiPublishRequest{
		Repository: "pingcap/tidb.insights",
		SourcePath: root,
	})

	require.Error(t, err)
	require.Contains(t, err.Error(), "exceeds 10 markdown pages or 2 filesystem entries")
}

type validatingAGSRepositoryStore struct {
	fakeAGSRepositoryStore
	liveSlugs map[string]struct{}
}

func (s *validatingAGSRepositoryStore) PutPage(ctx context.Context, request wiki.KBWikiPageWrite) error {
	if err := testAGSWikiSlugWritable(request.Page.Slug); err != nil {
		return err
	}
	if s.liveSlugs == nil {
		s.liveSlugs = map[string]struct{}{}
	}
	if conflict := testAGSWikiPrefixConflict(request.Page.Slug, s.liveSlugs); conflict != "" {
		return fmt.Errorf("conflict: wiki slug %q conflicts with existing page %q", request.Page.Slug, conflict)
	}
	s.liveSlugs[request.Page.Slug] = struct{}{}
	return s.fakeAGSRepositoryStore.PutPage(ctx, request)
}

func (s *validatingAGSRepositoryStore) PrunePages(ctx context.Context, request wiki.KBWikiPrune) error {
	if s.liveSlugs != nil {
		keep := map[string]struct{}{}
		for _, slug := range request.KeepSlugs {
			keep[slug] = struct{}{}
		}
		for slug := range s.liveSlugs {
			if _, ok := keep[slug]; !ok {
				delete(s.liveSlugs, slug)
			}
		}
	}
	return s.fakeAGSRepositoryStore.PrunePages(ctx, request)
}

func testHasSlugPrefix(slugs map[string]struct{}, prefix string) bool {
	for slug := range slugs {
		if strings.HasPrefix(slug, prefix) {
			return true
		}
	}
	return false
}

func testAGSWikiPrefixConflict(slug string, live map[string]struct{}) string {
	for existing := range live {
		if existing == slug {
			continue
		}
		if strings.HasPrefix(slug, existing+"/") || strings.HasPrefix(existing, slug+"/") {
			return existing
		}
	}
	return ""
}

func testAGSWikiSlugWritable(slug string) error {
	if slug == "" {
		return fmt.Errorf("empty slug")
	}
	if len(slug) > 255 {
		return fmt.Errorf("slug too long")
	}
	parts := strings.Split(slug, "/")
	if len(parts) > 6 {
		return fmt.Errorf("slug too deep")
	}
	for _, part := range parts {
		if part == "" {
			return fmt.Errorf("empty segment")
		}
		if part == "." || part == ".." {
			return fmt.Errorf("reserved segment")
		}
		if len(part) > 64 {
			return fmt.Errorf("segment too long")
		}
		if part == "_sidebar" {
			continue
		}
		for i, r := range part {
			switch {
			case r >= 'a' && r <= 'z':
			case r >= '0' && r <= '9':
			case r == '-' && i > 0:
			default:
				return fmt.Errorf("disallowed character %q", string(r))
			}
		}
		if part[0] == '-' {
			return fmt.Errorf("leading dash")
		}
	}
	return nil
}
