package testing_test

import (
	"bufio"
	"encoding/json"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"math"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"testing"
)

type coveragePolicy struct {
	TargetPercent float64            `json:"target_percent"`
	Files         map[string]float64 `json:"files"`
}

func TestSourceFileLineLimit(t *testing.T) {
	root := repoRoot(t)
	var offenders []string
	for _, relRoot := range []string{"cmd", "pkg", "scripts", "testing"} {
		walkSourceFiles(t, filepath.Join(root, relRoot), func(path string, info os.DirEntry) error {
			if info.IsDir() {
				return nil
			}
			switch filepath.Ext(path) {
			case ".go", ".py", ".sh":
				lines, err := countLines(path)
				if err != nil {
					return err
				}
				if lines > 500 {
					offenders = append(offenders, fmt.Sprintf("%s:%d", rel(root, path), lines))
				}
			}
			return nil
		})
	}
	if len(offenders) > 0 {
		sort.Strings(offenders)
		t.Fatalf("source files exceed 500 LOC: %s", strings.Join(offenders, ", "))
	}
}

func TestExportedDocs(t *testing.T) {
	root := repoRoot(t)
	var missing []string
	fset := token.NewFileSet()
	walkSourceFiles(t, root, func(path string, info os.DirEntry) error {
		if info.IsDir() || filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") {
			return nil
		}
		if !strings.HasPrefix(rel(root, path), "cmd/") && !strings.HasPrefix(rel(root, path), "pkg/") {
			return nil
		}
		file, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
		if err != nil {
			return err
		}
		for _, decl := range file.Decls {
			switch d := decl.(type) {
			case *ast.FuncDecl:
				if d.Name.IsExported() && !hasUsefulDoc(d.Doc, d.Name.Name) {
					missing = append(missing, fmt.Sprintf("%s:%s", rel(root, path), d.Name.Name))
				}
			case *ast.GenDecl:
				for _, spec := range d.Specs {
					switch s := spec.(type) {
					case *ast.TypeSpec:
						if s.Name.IsExported() && !hasUsefulDoc(d.Doc, s.Name.Name) {
							missing = append(missing, fmt.Sprintf("%s:%s", rel(root, path), s.Name.Name))
						}
					}
				}
			}
		}
		return nil
	})
	if len(missing) > 0 {
		sort.Strings(missing)
		t.Fatalf("exported declarations without useful docs: %s", strings.Join(missing, ", "))
	}
}

func TestGoFmtAndVet(t *testing.T) {
	root := repoRoot(t)
	gofmt := exec.Command("gofmt", "-l", "cmd", "pkg", "testing")
	gofmt.Dir = root
	out, err := gofmt.CombinedOutput()
	if err != nil {
		t.Fatalf("gofmt check failed: %v\n%s", err, out)
	}
	if trimmed := strings.TrimSpace(string(out)); trimmed != "" {
		t.Fatalf("gofmt -l reported files:\n%s", trimmed)
	}
	vet := exec.Command("go", "vet", "./...")
	vet.Dir = root
	out, err = vet.CombinedOutput()
	if err != nil {
		t.Fatalf("go vet failed: %v\n%s", err, out)
	}
}

func TestCoveragePolicy(t *testing.T) {
	root := repoRoot(t)
	coveragePath := filepath.Join(root, "build", "coverage.out")
	if err := os.MkdirAll(filepath.Dir(coveragePath), 0o755); err != nil {
		t.Fatalf("create coverage dir: %v", err)
	}
	useExisting := os.Getenv("METIS_USE_EXISTING_COVERAGE") == "1"
	if !useExisting {
		if err := os.Remove(coveragePath); err != nil && !os.IsNotExist(err) {
			t.Fatalf("clear stale coverage profile: %v", err)
		}
	}
	if _, err := os.Stat(coveragePath); err != nil {
		if !os.IsNotExist(err) {
			t.Fatalf("check coverage profile: %v", err)
		}
		cmd := exec.Command("go", "test", "-count=1", "./...", "-coverprofile=build/coverage.out")
		cmd.Dir = root
		out, runErr := cmd.CombinedOutput()
		if runErr != nil {
			t.Fatalf("root coverage run failed: %v\n%s", runErr, out)
		}
	}
	policyPath := filepath.Join(root, "testing", "coverage_policy.json")
	policy := loadCoveragePolicy(t, policyPath)
	actual := readCoverageProfile(t, coveragePath)
	var regressions []string
	for file, min := range policy.Files {
		got, ok := actual[file]
		if !ok {
			regressions = append(regressions, fmt.Sprintf("%s missing from coverage", file))
			continue
		}
		if got+0.05 < min {
			regressions = append(regressions, fmt.Sprintf("%s %.1f < %.1f", file, got, min))
		}
	}
	if len(regressions) > 0 {
		sort.Strings(regressions)
		t.Fatalf("coverage regressed: %s", strings.Join(regressions, ", "))
	}
}

func TestStructureHygiene(t *testing.T) {
	root := repoRoot(t)
	genericNames := map[string]struct{}{
		"tmp":   {},
		"temp":  {},
		"foo":   {},
		"bar":   {},
		"baz":   {},
		"misc":  {},
		"new":   {},
		"old":   {},
		"final": {},
		"wip":   {},
	}
	maxDepth := 8

	var violations []string
	for _, relRoot := range []string{"cmd", "pkg", "scripts", "testing"} {
		base := filepath.Join(root, relRoot)
		walkSourceFiles(t, base, func(path string, info os.DirEntry) error {
			if info.IsDir() {
				return nil
			}
			switch filepath.Ext(path) {
			case ".go", ".py", ".sh":
			default:
				return nil
			}
			relative := rel(root, path)
			depth := len(strings.Split(relative, "/"))
			if depth > maxDepth {
				violations = append(violations, fmt.Sprintf("%s: depth %d > %d", relative, depth, maxDepth))
			}

			baseName := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
			for _, tok := range strings.FieldsFunc(baseName, func(r rune) bool {
				return r == '_' || r == '-'
			}) {
				if _, found := genericNames[strings.ToLower(tok)]; found {
					violations = append(violations, fmt.Sprintf("%s: non-descriptive token %q", relative, tok))
				}
			}
			return nil
		})
	}
	if len(violations) > 0 {
		sort.Strings(violations)
		t.Fatalf("structure hygiene violations: %s", strings.Join(violations, ", "))
	}
}

func TestCodeSmellContracts(t *testing.T) {
	root := repoRoot(t)
	patterns := []struct {
		re      *regexp.Regexp
		scope   []string
		message string
	}{
		{
			re:      regexp.MustCompile(`\bpanic\(`),
			scope:   []string{"cmd", "pkg"},
			message: "avoid panic in production Go code",
		},
		{
			re:      regexp.MustCompile(`\blog\.Fatalf\(`),
			scope:   []string{"pkg"},
			message: "avoid log.Fatalf in pkg code",
		},
		{
			re:      regexp.MustCompile(`\bfmt\.Print(f|ln)?\(`),
			scope:   []string{"pkg"},
			message: "avoid fmt.Print* in pkg code",
		},
	}

	type hit struct {
		path string
		line int
		text string
		msg  string
	}
	var hits []hit
	for _, rule := range patterns {
		for _, relRoot := range rule.scope {
			base := filepath.Join(root, relRoot)
			walkSourceFiles(t, base, func(path string, info os.DirEntry) error {
				if info.IsDir() || filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") {
					return nil
				}
				file, err := os.Open(path)
				if err != nil {
					return err
				}
				defer file.Close()
				scanner := bufio.NewScanner(file)
				lineNo := 0
				for scanner.Scan() {
					lineNo++
					line := scanner.Text()
					if rule.re.MatchString(line) {
						hits = append(hits, hit{
							path: rel(root, path),
							line: lineNo,
							text: strings.TrimSpace(line),
							msg:  rule.message,
						})
					}
				}
				return scanner.Err()
			})
		}
	}

	if len(hits) > 0 {
		sort.Slice(hits, func(i, j int) bool {
			if hits[i].path == hits[j].path {
				return hits[i].line < hits[j].line
			}
			return hits[i].path < hits[j].path
		})
		lines := make([]string, 0, len(hits))
		for _, h := range hits {
			lines = append(lines, fmt.Sprintf("%s:%d (%s) %s", h.path, h.line, h.msg, h.text))
		}
		t.Fatalf("code smell violations: %s", strings.Join(lines, ", "))
	}
}

func countLines(path string) (int, error) {
	f, err := os.Open(path)
	if err != nil {
		return 0, err
	}
	defer f.Close()
	s := bufio.NewScanner(f)
	count := 0
	for s.Scan() {
		count++
	}
	return count, s.Err()
}

func rel(root, path string) string {
	out, err := filepath.Rel(root, path)
	if err != nil {
		return path
	}
	return filepath.ToSlash(out)
}

func hasUsefulDoc(comment *ast.CommentGroup, name string) bool {
	if comment == nil {
		return false
	}
	text := strings.TrimSpace(comment.Text())
	if text == "" {
		return false
	}
	if len(strings.Fields(text)) < 4 {
		return false
	}
	return strings.Contains(strings.ToLower(text), strings.ToLower(name[:1])) || len(text) > len(name)+12
}

func loadCoveragePolicy(t *testing.T, path string) coveragePolicy {
	t.Helper()
	data, err := os.ReadFile(path)
	if err != nil {
		t.Fatal(err)
	}
	var policy coveragePolicy
	if err := json.Unmarshal(data, &policy); err != nil {
		t.Fatal(err)
	}
	if policy.TargetPercent == 0 {
		policy.TargetPercent = 95
	}
	if policy.Files == nil {
		policy.Files = map[string]float64{}
	}
	return policy
}

func readCoverageProfile(t *testing.T, path string) map[string]float64 {
	t.Helper()
	f, err := os.Open(path)
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()
	stats := map[string]struct{ covered, total int }{}
	s := bufio.NewScanner(f)
	for s.Scan() {
		line := strings.TrimSpace(s.Text())
		if line == "" || strings.HasPrefix(line, "mode:") {
			continue
		}
		parts := strings.Fields(line)
		if len(parts) != 3 {
			continue
		}
		file := strings.SplitN(parts[0], ":", 2)[0]
		stmts, err := strconv.Atoi(parts[1])
		if err != nil {
			continue
		}
		count, err := strconv.Atoi(parts[2])
		if err != nil {
			continue
		}
		entry := stats[file]
		entry.total += stmts
		if count > 0 {
			entry.covered += stmts
		}
		stats[file] = entry
	}
	if err := s.Err(); err != nil {
		t.Fatal(err)
	}
	out := map[string]float64{}
	for file, stat := range stats {
		if stat.total == 0 {
			continue
		}
		out[file] = math.Round((float64(stat.covered)/float64(stat.total))*1000) / 10
	}
	return out
}
