package metadata

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

	"github.com/nexuscrm/shared/pkg/models"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/metadatair"
)

func TestNeutralIRDocExamplesMatchDocument(t *testing.T) {
	docPath := filepath.Join("..", "docs", "design", "l0-neutral-ir.md")
	raw, err := os.ReadFile(docPath)
	if err != nil {
		t.Fatalf("read neutral IR design doc: %v", err)
	}
	doc := string(raw)
	for key, generated := range neutralIRDocExampleBlocks(t) {
		if os.Getenv("UPDATE_METADATAIR_DOC") == "1" {
			doc = replaceGeneratedDocBlock(t, doc, key, generated)
			continue
		}
		got := extractGeneratedDocBlock(t, doc, key)
		if strings.TrimSpace(got) != strings.TrimSpace(generated) {
			t.Fatalf("generated block %q is stale\n--- got ---\n%s\n--- want ---\n%s", key, got, generated)
		}
	}
	if os.Getenv("UPDATE_METADATAIR_DOC") == "1" {
		if err := os.WriteFile(docPath, []byte(doc), 0o644); err != nil {
			t.Fatalf("write neutral IR design doc: %v", err)
		}
	}
}

func neutralIRDocExampleBlocks(t *testing.T) map[string]string {
	t.Helper()

	owner := metadatair.Owner{
		Layer:     metadatair.OwnerLayerL1,
		OwnerPack: "toy-workflow-pack",
	}
	objectSource := ObjectComponentIRSource{
		Owner:     owner,
		URI:       "pack://toy-workflow/objects/WorkItem.json",
		Component: "objects/WorkItem",
		Dialect:   "toy",
	}

	minimalObjectDoc, err := LowerObjectComponentHeaders(&models.ObjectMetadata{
		APIName: "WorkItem",
		Fields:  []models.FieldMetadata{{APIName: "Status"}},
	}, objectSource)
	if err != nil {
		t.Fatalf("lower minimal object IR doc: %v", err)
	}
	minimalObjectProjection, err := ProjectObjectFieldIdentityHeaders(minimalObjectDoc)
	if err != nil {
		t.Fatalf("project minimal object IR doc: %v", err)
	}

	objectDoc, err := LowerObjectComponentHeaders(&models.ObjectMetadata{
		APIName: "WorkItem",
		Fields: []models.FieldMetadata{
			{APIName: "Summary"},
			{APIName: "Status"},
		},
	}, objectSource)
	if err != nil {
		t.Fatalf("lower object IR doc: %v", err)
	}
	objectProjection, err := ProjectObjectFieldIdentityHeaders(objectDoc)
	if err != nil {
		t.Fatalf("project object IR doc: %v", err)
	}

	flowSource := FlowComponentIRSource{
		Owner:     owner,
		URI:       "pack://toy-workflow/flows/FulfillmentFlow.flow-meta.json",
		Component: "flows/FulfillmentFlow",
		Dialect:   "toy",
	}
	flowDefinition := &models.FlowDefinition{
		Id:            "fd-workflow",
		DeveloperName: "FulfillmentFlow",
		OwnerPack:     "toy-workflow-pack",
	}
	flowVersion := &models.FlowVersion{
		DefinitionId:  "fd-workflow",
		VersionNumber: 3,
		Metadata: models.FlowMetadata{
			Decisions:   []models.FlowDecision{{Name: "CheckStatus"}},
			Assignments: []models.FlowAssignment{{Name: "SetStatus"}},
			ActionCalls: []models.FlowActionCall{{Name: "NotifyOwner"}},
		},
	}
	flowDoc, err := LowerFlowComponentHeaders(flowDefinition, flowVersion, flowSource)
	if err != nil {
		t.Fatalf("lower flow IR doc: %v", err)
	}
	flowProjection, err := ProjectFlowElementIdentityHeaders(flowDoc, "FulfillmentFlow.v3")
	if err != nil {
		t.Fatalf("project flow IR doc: %v", err)
	}

	objectDuplicateErr := mustLoweringError(t, func() error {
		_, err := LowerObjectComponentHeaders(&models.ObjectMetadata{
			APIName: "WorkItem",
			Fields: []models.FieldMetadata{
				{APIName: "Status"},
				{APIName: "Status"},
			},
		}, objectSource)
		return err
	})
	flowDuplicateErr := mustLoweringError(t, func() error {
		_, err := LowerFlowComponentHeaders(
			&models.FlowDefinition{DeveloperName: "FulfillmentFlow"},
			&models.FlowVersion{
				VersionNumber: 2,
				Metadata: models.FlowMetadata{
					Decisions:   []models.FlowDecision{{Name: "SharedElement"}},
					Assignments: []models.FlowAssignment{{Name: "SharedElement"}},
				},
			},
			flowSource,
		)
		return err
	})
	ownerErr := mustLoweringError(t, func() error {
		badSource := objectSource
		badSource.Owner = metadatair.Owner{Layer: metadatair.OwnerLayerL1}
		_, err := LowerObjectComponentHeaders(&models.ObjectMetadata{APIName: "WorkItem"}, badSource)
		return err
	})

	return map[string]string{
		"minimal-object-nodes":      renderNodeTable(minimalObjectDoc.Nodes, "Owner"),
		"minimal-object-projection": renderObjectProjection(minimalObjectProjection),
		"field-header":              renderHeaderTable(minimalObjectDoc.Nodes[1]),
		"object-nodes":              renderNodeTable(objectDoc.Nodes, "Source format"),
		"object-projection":         renderObjectProjection(objectProjection),
		"flow-nodes":                renderNodeTable(flowDoc.Nodes, "Source format"),
		"flow-projection":           renderFlowProjection(flowProjection),
		"failure-output": renderTextBlock([]string{
			"duplicate object field: " + objectDuplicateErr,
			"duplicate flow element: " + flowDuplicateErr,
			"missing L1 owner pack: " + ownerErr,
		}),
	}
}

func renderNodeTable(nodes []metadatair.NodeHeader, thirdColumn string) string {
	var b strings.Builder
	fmt.Fprintf(&b, "| Kind | ID | %s | Source path |\n", thirdColumn)
	b.WriteString("|---|---|---|---|\n")
	for _, node := range nodes {
		thirdValue := string(node.Source.Format)
		if thirdColumn == "Owner" {
			thirdValue = renderOwner(node.Owner)
		}
		fmt.Fprintf(&b, "| `%s` | `%s` | `%s` | `%s` |\n", node.Kind, node.ID, thirdValue, node.Source.Path)
	}
	return strings.TrimRight(b.String(), "\n")
}

func renderHeaderTable(node metadatair.NodeHeader) string {
	rows := [][2]string{
		{"ID", string(node.ID)},
		{"Kind", string(node.Kind)},
		{"Owner", renderOwner(node.Owner)},
		{"Source format", string(node.Source.Format)},
		{"Source URI", node.Source.URI},
		{"Source component", node.Source.Component},
		{"Source path", node.Source.Path},
		{"Dialect", node.Source.Dialect},
		{"Lowering boundary", fmt.Sprintf("%s -> %s", node.Boundary.From, node.Boundary.To)},
	}
	var b strings.Builder
	b.WriteString("| Header field | Generated value |\n")
	b.WriteString("|---|---|\n")
	for _, row := range rows {
		fmt.Fprintf(&b, "| %s | `%s` |\n", row[0], row[1])
	}
	return strings.TrimRight(b.String(), "\n")
}

func renderObjectProjection(projection ObjectFieldIdentityProjection) string {
	return fencedGo(fmt.Sprintf("metadata.ObjectFieldIdentityProjection{\n\tObjectAPIName: %q,\n\tFieldAPINames: []string{%s},\n}", projection.ObjectAPIName, renderQuotedList(projection.FieldAPINames)))
}

func renderFlowProjection(projection FlowElementIdentityProjection) string {
	return fencedGo(fmt.Sprintf("metadata.FlowElementIdentityProjection{\n\tVersionNodeID: %q,\n\tElementNames: []string{%s},\n}", projection.VersionNodeID, renderQuotedList(projection.ElementNames)))
}

func renderTextBlock(lines []string) string {
	return "```text\n" + strings.Join(lines, "\n") + "\n```"
}

func fencedGo(body string) string {
	return "```go\n" + body + "\n```"
}

func renderQuotedList(values []string) string {
	out := make([]string, len(values))
	for i, value := range values {
		out[i] = strconv.Quote(value)
	}
	return strings.Join(out, ", ")
}

func renderOwner(owner metadatair.Owner) string {
	if owner.OwnerPack == "" {
		return string(owner.Layer)
	}
	return string(owner.Layer) + ":" + owner.OwnerPack
}

func mustLoweringError(t *testing.T, fn func() error) string {
	t.Helper()
	err := fn()
	if err == nil {
		t.Fatal("expected lowering error")
	}
	return err.Error()
}

func extractGeneratedDocBlock(t *testing.T, doc, key string) string {
	t.Helper()
	start, end := generatedDocMarkers(key)
	startIndex := strings.Index(doc, start)
	if startIndex < 0 {
		t.Fatalf("missing generated block start marker %q", start)
	}
	contentStart := startIndex + len(start)
	endIndex := strings.Index(doc[contentStart:], end)
	if endIndex < 0 {
		t.Fatalf("missing generated block end marker %q", end)
	}
	return strings.Trim(doc[contentStart:contentStart+endIndex], "\n")
}

func replaceGeneratedDocBlock(t *testing.T, doc, key, generated string) string {
	t.Helper()
	start, end := generatedDocMarkers(key)
	startIndex := strings.Index(doc, start)
	if startIndex < 0 {
		t.Fatalf("missing generated block start marker %q", start)
	}
	contentStart := startIndex + len(start)
	endIndex := strings.Index(doc[contentStart:], end)
	if endIndex < 0 {
		t.Fatalf("missing generated block end marker %q", end)
	}
	contentEnd := contentStart + endIndex
	return doc[:contentStart] + "\n" + strings.TrimSpace(generated) + "\n" + doc[contentEnd:]
}

func generatedDocMarkers(key string) (string, string) {
	return "<!-- generated:metadatair:" + key + ":start -->", "<!-- generated:metadatair:" + key + ":end -->"
}
