package testmark

import (
	"fmt"
	"go/parser"
	"go/token"
	"io/fs"
	"regexp"
	"strings"
	"testing"
)

// DocTest extracts and tests Go code snippets from markdown documentation.
type DocTest struct {
	// PackageTemplate is the Go package template for wrapping code snippets
	PackageTemplate string

	// ImportTemplate is the import template for code snippets
	ImportTemplate string

	// TestTimeout configures how long to wait for code execution
	TestTimeout string

	// SkipPatterns are regex patterns for code blocks to skip
	SkipPatterns []*regexp.Regexp
}

// DefaultDocTest returns a DocTest with sensible defaults for testing Go snippets.
func DefaultDocTest() *DocTest {
	return &DocTest{
		PackageTemplate: `package main

import (
	"fmt"
	"strings"
	testmark "github.com/warpfork/go-testmark/v2"
	%s
)

func main() {
%s
}`,
		ImportTemplate: "",
		TestTimeout:    "30s",
		SkipPatterns: []*regexp.Regexp{
			regexp.MustCompile(`(?i)// skip|// todo|// fixme`),
			regexp.MustCompile(`(?i)func Test|func Example`), // Don't run test functions as main
		},
	}
}

// TestDocumentation scans documentation files for Go code blocks and tests them.
func (dt *DocTest) TestDocumentation(t *testing.T, filesystem fs.FS, patterns ...string) {
	if len(patterns) == 0 {
		patterns = []string{"*.md", "**/README.md"}
	}

	for _, pattern := range patterns {
		matches, err := fs.Glob(filesystem, pattern)
		if err != nil {
			t.Errorf("failed to glob pattern %s: %v", pattern, err)
			continue
		}

		for _, match := range matches {
			t.Run(match, func(t *testing.T) {
				dt.testDocumentationFile(t, filesystem, match)
			})
		}
	}
}

// testDocumentationFile tests a single documentation file
func (dt *DocTest) testDocumentationFile(t *testing.T, filesystem fs.FS, filename string) {
	f, err := filesystem.Open(filename)
	if err != nil {
		t.Fatalf("failed to open %s: %v", filename, err)
	}
	defer f.Close()

	// Parse the markdown file
	doc, err := Read(f)
	if err != nil {
		// If it's not a testmark file, try to parse it as regular markdown
		dt.testRegularMarkdown(t, filesystem, filename)
		return
	}

	// Test Go code blocks in testmark hunks
	dt.testTestmarkHunks(t, doc, filename)
}

// testTestmarkHunks tests Go code in testmark hunks
func (dt *DocTest) testTestmarkHunks(t *testing.T, doc *Document, filename string) {
	for iter := doc.Hunks(); iter.Next(); {
		hunk := iter.Hunk()

		// Only test Go code blocks
		if !dt.IsGoCode(hunk.InfoString(), hunk.String()) {
			continue
		}

		// Skip if matches skip patterns
		if dt.ShouldSkip(hunk.String()) {
			continue
		}

		t.Run(hunk.Name(), func(t *testing.T) {
			dt.testGoCode(t, hunk.String(), fmt.Sprintf("%s:%s", filename, hunk.Name()))
		})
	}
}

// testRegularMarkdown tests Go code blocks in regular markdown
func (dt *DocTest) testRegularMarkdown(t *testing.T, filesystem fs.FS, filename string) {
	// For regular markdown, we'd need a markdown parser
	// For now, we'll implement a simple code block extractor
	f, err := filesystem.Open(filename)
	if err != nil {
		t.Fatalf("failed to open %s: %v", filename, err)
	}
	defer f.Close()

	content, err := fs.ReadFile(filesystem, filename)
	if err != nil {
		t.Fatalf("failed to read %s: %v", filename, err)
	}

	codeBlocks := dt.ExtractGoCodeBlocks(string(content))
	for i, code := range codeBlocks {
		if dt.ShouldSkip(code) {
			continue
		}

		t.Run(fmt.Sprintf("code-block-%d", i+1), func(t *testing.T) {
			dt.testGoCode(t, code, fmt.Sprintf("%s:block-%d", filename, i+1))
		})
	}
}

// extractGoCodeBlocks extracts Go code blocks from markdown content
func (dt *DocTest) ExtractGoCodeBlocks(content string) []string {
	var blocks []string
	lines := strings.Split(content, "\n")
	var inCodeBlock bool
	var currentBlock strings.Builder
	var blockLang string

	for _, line := range lines {
		if strings.HasPrefix(line, "```") {
			if !inCodeBlock {
				// Starting a code block
				blockLang = strings.TrimSpace(line[3:])
				inCodeBlock = true
				currentBlock.Reset()
			} else {
				// Ending a code block
				inCodeBlock = false
				if dt.IsGoCode(blockLang, currentBlock.String()) {
					blocks = append(blocks, strings.TrimSpace(currentBlock.String()))
				}
			}
		} else if inCodeBlock {
			currentBlock.WriteString(line + "\n")
		}
	}

	return blocks
}

// IsGoCode determines if a code block contains Go code
func (dt *DocTest) IsGoCode(lang, code string) bool {
	// Check language hint
	if lang == "go" || lang == "golang" {
		return true
	}

	// Heuristic: look for Go-like syntax
	if strings.Contains(code, "package ") ||
		strings.Contains(code, "import ") ||
		strings.Contains(code, "func ") ||
		strings.Contains(code, ":=") ||
		strings.Contains(code, "testmark.") {
		return true
	}

	return false
}

// shouldSkip checks if code should be skipped based on skip patterns
func (dt *DocTest) ShouldSkip(code string) bool {
	for _, pattern := range dt.SkipPatterns {
		if pattern.MatchString(code) {
			return true
		}
	}
	return false
}

// testGoCode tests a single Go code snippet
func (dt *DocTest) testGoCode(t *testing.T, code, location string) {
	// First, check if it's valid Go syntax
	if !dt.IsValidGoSyntax(code) {
		// Try wrapping it in a main function
		wrapped := dt.WrapCode(code)
		if !dt.IsValidGoSyntax(wrapped) {
			t.Errorf("invalid Go syntax in %s", location)
			return
		}
		code = wrapped
	}

	// Create a test entry and run it
	entry := &DirEntry{
		name:     "doctest",
		path:     "doctest",
		children: make(map[string]*DirEntry),
		childrenOrdered: []*DirEntry{
			{
				name: "script",
				path: "doctest/script",
				hunk: &Hunk{
					name: "doctest/script",
					body: []byte(code),
				},
			},
		},
	}
	entry.children["script"] = entry.childrenOrdered[0]

	// Run with Go-aware execution
	config := DefaultExecConfig()
	config.ExecFn = SmartGoExecFunc()

	// Capture any panic and convert to test failure
	defer func() {
		if r := recover(); r != nil {
			t.Errorf("code execution failed in %s: %v", location, r)
		}
	}()

	TestExec(t, entry, config)
}

// isValidGoSyntax checks if code is syntactically valid Go
func (dt *DocTest) IsValidGoSyntax(code string) bool {
	fset := token.NewFileSet()
	_, err := parser.ParseFile(fset, "test.go", code, parser.ParseComments)
	return err == nil
}

// wrapCode wraps code snippets that aren't complete programs
func (dt *DocTest) WrapCode(code string) string {
	return fmt.Sprintf(dt.PackageTemplate, dt.ImportTemplate, dt.indentCode(code))
}

// indentCode indents code for wrapping in templates
func (dt *DocTest) indentCode(code string) string {
	lines := strings.Split(strings.TrimSpace(code), "\n")
	for i, line := range lines {
		if strings.TrimSpace(line) != "" {
			lines[i] = "\t" + line
		}
	}
	return strings.Join(lines, "\n")
}

// Convenience functions

// TestReadmeExamples tests Go code examples in README files.
func TestReadmeExamples(t *testing.T, filesystem fs.FS) {
	dt := DefaultDocTest()
	dt.TestDocumentation(t, filesystem, "README.md", "**/README.md")
}

// TestAllDocExamples tests Go code examples in all markdown files.
func TestAllDocExamples(t *testing.T, filesystem fs.FS) {
	dt := DefaultDocTest()
	dt.TestDocumentation(t, filesystem, "*.md", "**/*.md")
}
