package outline

import (
	"bytes"
	"encoding/csv"
	"encoding/json"
	"fmt"
	"go/ast"
	"go/token"
	"strconv"
	"strings"

	"golang.org/x/tools/go/ast/inspector"
)

const (
	// ginkgoImportPath is the well-known ginkgo import path
	ginkgoImportPath = "github.com/onsi/ginkgo/v2"
)

// FromASTFile returns an outline for a Ginkgo test source file
func FromASTFile(fset *token.FileSet, src *ast.File) (*outline, error) {
	ginkgoPackageName := packageNameForImport(src, ginkgoImportPath)
	if ginkgoPackageName == nil {
		return nil, fmt.Errorf("file does not import %q", ginkgoImportPath)
	}

	root := ginkgoNode{}
	stack := []*ginkgoNode{&root}
	ispr := inspector.New([]*ast.File{src})
	ispr.Nodes([]ast.Node{(*ast.CallExpr)(nil)}, func(node ast.Node, push bool) bool {
		if push {
			// Pre-order traversal
			ce, ok := node.(*ast.CallExpr)
			if !ok {
				// Because `Nodes` calls this function only when the node is an
				// ast.CallExpr, this should never happen
				panic(fmt.Errorf("node starting at %d, ending at %d is not an *ast.CallExpr", node.Pos(), node.End()))
			}
			gn, ok := ginkgoNodeFromCallExpr(fset, ce, ginkgoPackageName)
			if !ok {
				// Node is not a Ginkgo spec or container, continue
				return true
			}
			parent := stack[len(stack)-1]
			parent.Nodes = append(parent.Nodes, gn)
			stack = append(stack, gn)
			return true
		}
		// Post-order traversal
		start, end := absoluteOffsetsForNode(fset, node)
		lastVisitedGinkgoNode := stack[len(stack)-1]
		if start != lastVisitedGinkgoNode.Start || end != lastVisitedGinkgoNode.End {
			// Node is not a Ginkgo spec or container, so it was not pushed onto the stack, continue
			return true
		}
		expandSubtree(lastVisitedGinkgoNode)
		stack = stack[0 : len(stack)-1]
		return true
	})
	if len(root.Nodes) == 0 {
		return &outline{[]*ginkgoNode{}}, nil
	}

	// Derive the final focused property for all nodes. This must be done
	// _before_ propagating the inherited focused property.
	root.BackpropagateUnfocus()
	// Now, propagate inherited properties, including focused and pending.
	root.PropagateInheritedProperties()

	return &outline{root.Nodes}, nil
}

type outline struct {
	Nodes []*ginkgoNode `json:"nodes"`
}

func (o *outline) MarshalJSON() ([]byte, error) {
	return json.Marshal(o.Nodes)
}

// String returns a CSV-formatted outline. Spec or container are output in
// depth-first order.
func (o *outline) String() string {
	return o.StringIndent(0)
}

// StringIndent returns a CSV-formated outline, but every line is indented by
// one 'width' of spaces for every level of nesting.
func (o *outline) StringIndent(width int) string {
	var b bytes.Buffer
	b.WriteString("Name,Text,Start,End,Spec,Focused,Pending,Labels\n")

	csvWriter := csv.NewWriter(&b)

	currentIndent := 0
	pre := func(n *ginkgoNode) {
		b.WriteString(fmt.Sprintf("%*s", currentIndent, ""))
		var labels string
		if len(n.Labels) == 1 {
			labels = n.Labels[0]
		} else {
			labels = strings.Join(n.Labels, ", ")
		}

		row := []string{
			n.Name,
			n.Text,
			strconv.Itoa(n.Start),
			strconv.Itoa(n.End),
			strconv.FormatBool(n.Spec),
			strconv.FormatBool(n.Focused),
			strconv.FormatBool(n.Pending),
			labels,
		}
		csvWriter.Write(row)

		// Ensure we write to `b' before the next `b.WriteString()', which might be adding indentation
		csvWriter.Flush()

		currentIndent += width
	}
	post := func(n *ginkgoNode) {
		currentIndent -= width
	}
	for _, n := range o.Nodes {
		n.Walk(pre, post)
	}

	return b.String()
}

// expandSubtree restructures a DescribeTableSubtree node so that each Entry
// child gets a copy of the subtree's spec nodes as its children. This mirrors
// the runtime behavior where each Entry generates a container with the specs
// defined in the DescribeTableSubtree body.
func expandSubtree(gn *ginkgoNode) {
	if !strings.Contains(gn.Name, "DescribeTableSubtree") {
		return
	}
	subNodes, entries := splitSubtreeSubnodes(gn.Nodes)
	gn.Nodes = entries
	for _, entry := range entries {
		entry.Nodes = subNodes
	}
}

// splitSubtreeSubnodes splits the child nodes of a DescribeTableSubtree into
// spec/container nodes (defined in the body) and Entry nodes.
func splitSubtreeSubnodes(nodes []*ginkgoNode) ([]*ginkgoNode, []*ginkgoNode) {
	for i, node := range nodes {
		if strings.Contains(node.Name, "Entry") {
			return nodes[:i], nodes[i:]
		}
	}
	return nodes, nil
}
