// Package doctest provides experimental support for testing Go code snippets extracted from documentation.
//
// This package is experimental and its API may change. It extracts Go code blocks from
// markdown files and tests them by compiling and running them.
//
// Basic usage:
//
//	testdocs := doctest.New()
//	testdocs.TestReadme(t, os.DirFS("."))
package doctest

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

	"github.com/warpfork/go-testmark/v2"
	// TODO: re-enable when testexec integration is implemented
	// "github.com/warpfork/go-testmark/v2/testexec"
)

// Tester extracts and tests Go code snippets from markdown documentation.
type Tester 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
}

// New returns a Tester with sensible defaults for testing Go snippets.
func New() *Tester {
	return &Tester{
		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 *Tester) 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 *Tester) 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
	data, err := f.(fs.File).Stat()
	if err != nil {
		// If it's not a testmark file, try to parse it as regular markdown
		dt.testRegularMarkdown(t, filesystem, filename)
		return
	}
	_ = data // TODO: implement proper parsing

	// TODO: implement testmark hunk testing
	_ = filename // suppress unused warning
}

// testTestmarkHunks tests Go code in testmark hunks
// TODO: implement this properly with the new API
func (dt *Tester) testTestmarkHunks(t *testing.T, doc *testmark.Document, filename string) {
	// Stub implementation
	_ = doc
	_ = filename
	t.Skip("testTestmarkHunks not implemented yet")
}

// testRegularMarkdown tests Go code blocks in regular markdown
func (dt *Tester) 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 *Tester) 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 *Tester) 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.") ||
		strings.Contains(code, "fmt.") ||
		strings.Contains(code, "log.") ||
		strings.Contains(code, "errors.") ||
		strings.Contains(code, "context.") ||
		strings.Contains(code, "time.") {
		return true
	}

	return false
}

// shouldSkip checks if code should be skipped based on skip patterns
func (dt *Tester) 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 *Tester) testGoCode(t *testing.T, code, location string) {
	// TODO: reimplement this with the new API
	_ = code
	_ = location
	t.Skip("testGoCode not implemented yet - needs redesign")
}

// isValidGoSyntax checks if code is syntactically valid Go
func (dt *Tester) 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 *Tester) WrapCode(code string) string {
	return fmt.Sprintf(dt.PackageTemplate, dt.ImportTemplate, dt.indentCode(code))
}

// indentCode indents code for wrapping in templates
func (dt *Tester) 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

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

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