package main

import (
	"encoding/json"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"testing"
)

const appFactInventoryRel = "docs/refactor/serviceification/app-fact-consumer-inventory.json"

type appFactInventory struct {
	SchemaVersion           string                           `json:"schema_version"`
	GeneratedAt             string                           `json:"generated_at"`
	Purpose                 string                           `json:"purpose"`
	Repositories            map[string]string                `json:"repositories"`
	NexusCRMConsumptionPins map[string]appFactConsumptionPin `json:"nexuscrm_consumption_pins"`
	ClassificationValues    []string                         `json:"classification_values"`
	Facts                   []appFact                        `json:"facts"`
}

type appFactConsumptionPin struct {
	Version                  string `json:"version"`
	Commit                   string `json:"commit"`
	AuditedMainPathsMatchPin bool   `json:"audited_main_paths_match_pin"`
}

type appFact struct {
	ID                              string         `json:"id"`
	Name                            string         `json:"name"`
	Author                          string         `json:"author"`
	SoleIntendedOwner               string         `json:"sole_intended_owner"`
	PersistedRepresentation         []string       `json:"persisted_representation"`
	CurrentWriters                  []string       `json:"current_writers"`
	CurrentReaders                  []string       `json:"current_readers"`
	LifecycleVersion                string         `json:"lifecycle_version"`
	Classification                  string         `json:"classification"`
	DuplicateOrCompetingPaths       []string       `json:"duplicate_or_competing_paths"`
	MigrationOrDeletionPrerequisite string         `json:"migration_or_deletion_prerequisite"`
	Evidence                        []appFactProof `json:"evidence"`
}

type appFactProof struct {
	Repository string `json:"repository"`
	Path       string `json:"path"`
	Lines      string `json:"lines"`
}

var (
	appFactIDPattern    = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`)
	commitSHA1Pattern   = regexp.MustCompile(`^[0-9a-f]{40}$`)
	evidenceLinePattern = regexp.MustCompile(`^([1-9][0-9]*)-([1-9][0-9]*)$`)
)

func TestAppFactConsumerInventory(t *testing.T) {
	root, err := detectRepoRoot()
	if err != nil {
		t.Fatal(err)
	}
	body, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(appFactInventoryRel)))
	if err != nil {
		t.Fatal(err)
	}
	var inventory appFactInventory
	decoder := json.NewDecoder(strings.NewReader(string(body)))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&inventory); err != nil {
		t.Fatalf("decode %s: %v", appFactInventoryRel, err)
	}
	if err := decoder.Decode(&struct{}{}); err != io.EOF {
		t.Fatalf("decode %s trailing content: %v", appFactInventoryRel, err)
	}
	if inventory.SchemaVersion != "nexuscrm.appFactConsumerInventory.v1" {
		t.Fatalf("schema_version = %q", inventory.SchemaVersion)
	}
	if strings.TrimSpace(inventory.GeneratedAt) == "" || strings.TrimSpace(inventory.Purpose) == "" {
		t.Fatal("generated_at and purpose are required")
	}

	requiredRepos := []string{"NexusCRM", "nexuscrm-controlplane", "nexuscrm-app-packs", "nexuscrm-contract"}
	for _, repository := range requiredRepos {
		if !commitSHA1Pattern.MatchString(inventory.Repositories[repository]) {
			t.Errorf("repository %s does not have an exact 40-character commit", repository)
		}
	}
	if len(inventory.Repositories) != len(requiredRepos) {
		t.Errorf("repositories = %d, want exactly the four audited repositories", len(inventory.Repositories))
	}
	for _, repository := range []string{"nexuscrm-controlplane", "nexuscrm-contract"} {
		pin, ok := inventory.NexusCRMConsumptionPins[repository]
		if !ok {
			t.Errorf("nexuscrm_consumption_pins missing %s", repository)
			continue
		}
		if strings.TrimSpace(pin.Version) == "" || !commitSHA1Pattern.MatchString(pin.Commit) {
			t.Errorf("invalid NexusCRM consumption pin for %s", repository)
		}
		if !pin.AuditedMainPathsMatchPin {
			t.Errorf("audited main paths do not match the consumed %s pin", repository)
		}
	}
	if len(inventory.NexusCRMConsumptionPins) != 2 {
		t.Errorf("nexuscrm_consumption_pins = %d, want exactly the consumed controlplane and contract modules", len(inventory.NexusCRMConsumptionPins))
	}

	allowedClassifications := make(map[string]bool, len(inventory.ClassificationValues))
	for _, value := range inventory.ClassificationValues {
		if value == "" || allowedClassifications[value] {
			t.Fatalf("invalid duplicate classification value %q", value)
		}
		allowedClassifications[value] = true
	}
	for _, required := range []string{"authoritative", "derived", "transitional", "stale", "target-only"} {
		if !allowedClassifications[required] {
			t.Errorf("classification_values missing %q", required)
		}
	}

	requiredFacts := map[string]bool{
		"pack-manifest-authoring":                     false,
		"pack-source-identity":                        false,
		"installed-pack-registry":                     false,
		"installed-pack-runtime-declarations":         false,
		"known-installed-pack-catalog":                false,
		"active-app-pack-projection":                  false,
		"target-app-package":                          false,
		"target-app-manifest":                         false,
		"target-app-release":                          false,
		"target-app-installation":                     false,
		"runtime-security-installation-projection":    false,
		"runtime-delegation-app-installation-carrier": false,
		"pack-host-requirements":                      false,
		"connector-capability-declarations":           false,
		"product-hooks-capabilities":                  false,
		"selected-root-runtime-capability":            false,
		"flow-action-handler-binding":                 false,
		"runtime-route-and-dialect-bindings":          false,
		"frontend-source-supply":                      false,
		"frontend-runtime-contribution":               false,
		"system-app-navigation-config":                false,
	}
	seen := map[string]bool{}
	for index, fact := range inventory.Facts {
		label := fmt.Sprintf("facts[%d]", index)
		if !appFactIDPattern.MatchString(fact.ID) {
			t.Errorf("%s id = %q", label, fact.ID)
		}
		if seen[fact.ID] {
			t.Errorf("duplicate fact id %q", fact.ID)
		}
		seen[fact.ID] = true
		if _, required := requiredFacts[fact.ID]; required {
			requiredFacts[fact.ID] = true
		}
		for field, value := range map[string]string{
			"name": fact.Name, "author": fact.Author, "sole_intended_owner": fact.SoleIntendedOwner,
			"lifecycle_version": fact.LifecycleVersion, "classification": fact.Classification,
			"migration_or_deletion_prerequisite": fact.MigrationOrDeletionPrerequisite,
		} {
			if strings.TrimSpace(value) == "" {
				t.Errorf("%s.%s is empty", label, field)
			}
		}
		if !allowedClassifications[fact.Classification] {
			t.Errorf("%s classification %q is not declared", label, fact.Classification)
		}
		for field, values := range map[string][]string{
			"persisted_representation":     fact.PersistedRepresentation,
			"current_writers":              fact.CurrentWriters,
			"current_readers":              fact.CurrentReaders,
			"duplicate_or_competing_paths": fact.DuplicateOrCompetingPaths,
		} {
			if len(values) == 0 {
				t.Errorf("%s.%s is empty", label, field)
			}
			for valueIndex, value := range values {
				if strings.TrimSpace(value) == "" {
					t.Errorf("%s.%s[%d] is blank", label, field, valueIndex)
				}
			}
		}
		if len(fact.Evidence) == 0 {
			t.Errorf("%s.evidence is empty", label)
		}
		for proofIndex, proof := range fact.Evidence {
			proofLabel := fmt.Sprintf("%s.evidence[%d]", label, proofIndex)
			if _, ok := inventory.Repositories[proof.Repository]; !ok {
				t.Errorf("%s repository %q is outside the audited revisions", proofLabel, proof.Repository)
			}
			if strings.TrimSpace(proof.Path) == "" || filepath.IsAbs(proof.Path) || strings.Contains(proof.Path, "..") {
				t.Errorf("%s path = %q", proofLabel, proof.Path)
			}
			match := evidenceLinePattern.FindStringSubmatch(proof.Lines)
			if match == nil {
				t.Errorf("%s lines = %q", proofLabel, proof.Lines)
				continue
			}
			start, _ := strconv.Atoi(match[1])
			end, _ := strconv.Atoi(match[2])
			if end < start {
				t.Errorf("%s lines run backwards: %s", proofLabel, proof.Lines)
			}
			if proof.Repository == "NexusCRM" {
				proofBody, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(proof.Path)))
				if err != nil {
					t.Errorf("%s cannot read current-repository evidence: %v", proofLabel, err)
					continue
				}
				lineCount := strings.Count(string(proofBody), "\n")
				if len(proofBody) > 0 && proofBody[len(proofBody)-1] != '\n' {
					lineCount++
				}
				if end > lineCount {
					t.Errorf("%s lines %s exceed %s line count %d", proofLabel, proof.Lines, proof.Path, lineCount)
				}
			}
		}
	}
	for id, present := range requiredFacts {
		if !present {
			t.Errorf("required fact %q is missing", id)
		}
	}
}
