package modules_test

import (
	"fmt"
	"go/parser"
	"go/token"
	"io/fs"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"testing"
)

const moduleImportPrefix = "gitea.maximumdirect.net/eric/notarius/internal/modules/"
const internalImportPrefix = "gitea.maximumdirect.net/eric/notarius/internal/"

func TestProductionImportBoundaries(t *testing.T) {
	repositoryRoot := testRepositoryRoot(t)
	err := filepath.WalkDir(repositoryRoot, func(path string, entry fs.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if entry.IsDir() {
			if entry.Name() == ".git" || entry.Name() == "testdata" || entry.Name() == "vendor" {
				return filepath.SkipDir
			}
			return nil
		}
		if filepath.Ext(path) != ".go" {
			return nil
		}
		return checkImportBoundaries(repositoryRoot, path)
	})
	if err != nil {
		t.Fatal(err)
	}
}

func TestImportBoundaryFixtureIsRejected(t *testing.T) {
	repositoryRoot := testRepositoryRoot(t)
	fixture := filepath.Join(repositoryRoot, "internal", "modules", "generic", "testdata", "importboundaries", "imports_dnd.go")
	err := checkImportBoundaries(repositoryRoot, fixture)
	if err == nil {
		t.Fatal("fixture import was accepted, want generic-to-concrete violation")
	}
	if !strings.Contains(err.Error(), "generic family must not import concrete family") {
		t.Fatalf("fixture error = %q, want generic-to-concrete violation", err)
	}
}

func TestImportBoundaryRules(t *testing.T) {
	tests := []struct {
		name          string
		filename      string
		sourcePackage string
		importPath    string
		wantError     bool
	}{
		{
			name:          "concrete implementation cannot import peer concrete family",
			filename:      "internal/modules/dnd/extract/example/extractor.go",
			sourcePackage: "example",
			importPath:    moduleImportPrefix + "seriatim/input/transcript",
			wantError:     true,
		},
		{
			name:          "future concrete family cannot import current concrete family",
			filename:      "internal/modules/almanac/extract/example/extractor.go",
			sourcePackage: "example",
			importPath:    moduleImportPrefix + "dnd/shared",
			wantError:     true,
		},
		{
			name:          "current concrete family cannot import future concrete family",
			filename:      "internal/modules/seriatim/input/example/adapter.go",
			sourcePackage: "example",
			importPath:    moduleImportPrefix + "almanac/shared",
			wantError:     true,
		},
		{
			name:          "generic implementation cannot import current concrete family",
			filename:      "internal/modules/generic/merge/example/merger.go",
			sourcePackage: "example",
			importPath:    moduleImportPrefix + "dnd/shared",
			wantError:     true,
		},
		{
			name:          "generic implementation cannot import future concrete family",
			filename:      "internal/modules/generic/merge/example/merger.go",
			sourcePackage: "example",
			importPath:    moduleImportPrefix + "almanac/shared",
			wantError:     true,
		},
		{
			name:          "concrete implementation cannot import generic implementation",
			filename:      "internal/modules/dnd/extract/example/extractor.go",
			sourcePackage: "example",
			importPath:    moduleImportPrefix + "generic/normalize/noop",
			wantError:     true,
		},
		{
			name:          "concrete white-box test follows production rules",
			filename:      "internal/modules/dnd/extract/example/extractor_test.go",
			sourcePackage: "example",
			importPath:    moduleImportPrefix + "generic/normalize/noop",
			wantError:     true,
		},
		{
			name:          "concrete registrar may compose generic implementation",
			filename:      "internal/modules/almanac/register/register.go",
			sourcePackage: "register",
			importPath:    moduleImportPrefix + "generic/normalize/noop",
		},
		{
			name:          "family root cannot import child implementation",
			filename:      "internal/modules/almanac/types.go",
			sourcePackage: "almanac",
			importPath:    moduleImportPrefix + "almanac/extract/events",
			wantError:     true,
		},
		{
			name:          "family root cannot import registrar",
			filename:      "internal/modules/almanac/types.go",
			sourcePackage: "almanac",
			importPath:    moduleImportPrefix + "almanac/register",
			wantError:     true,
		},
		{
			name:          "child may import family root",
			filename:      "internal/modules/almanac/extract/events/extractor.go",
			sourcePackage: "events",
			importPath:    moduleImportPrefix + "almanac",
		},
		{
			name:          "child may import same-family sibling",
			filename:      "internal/modules/almanac/extract/events/extractor.go",
			sourcePackage: "events",
			importPath:    moduleImportPrefix + "almanac/shared",
		},
		{
			name:          "concrete registrar may compose own child",
			filename:      "internal/modules/almanac/register/register.go",
			sourcePackage: "register",
			importPath:    moduleImportPrefix + "almanac/extract/events",
		},
		{
			name:          "generic registrar may compose generic child",
			filename:      "internal/modules/generic/register/register.go",
			sourcePackage: "register",
			importPath:    moduleImportPrefix + "generic/output/json",
		},
		{
			name:          "application composition root may compose registrars",
			filename:      "internal/cli/catalog.go",
			sourcePackage: "cli",
			importPath:    moduleImportPrefix + "almanac/register",
		},
		{
			name:          "production CLI cannot import family root",
			filename:      "internal/cli/catalog.go",
			sourcePackage: "cli",
			importPath:    moduleImportPrefix + "almanac",
			wantError:     true,
		},
		{
			name:          "production CLI cannot import concrete leaf",
			filename:      "internal/cli/catalog.go",
			sourcePackage: "cli",
			importPath:    moduleImportPrefix + "almanac/extract/events",
			wantError:     true,
		},
		{
			name:          "production CLI cannot import generic leaf",
			filename:      "internal/cli/catalog.go",
			sourcePackage: "cli",
			importPath:    moduleImportPrefix + "generic/output/json",
			wantError:     true,
		},
		{
			name:          "production CLI cannot import non-registrar package",
			filename:      "internal/cli/catalog.go",
			sourcePackage: "cli",
			importPath:    moduleImportPrefix + "almanac/register/helpers",
			wantError:     true,
		},
		{
			name:          "CLI test may import concrete leaf",
			filename:      "internal/cli/compatibility_test.go",
			sourcePackage: "cli",
			importPath:    moduleImportPrefix + "almanac/extract/events",
		},
		{
			name:          "command production cannot import registrar",
			filename:      "cmd/notarius/main.go",
			sourcePackage: "main",
			importPath:    moduleImportPrefix + "almanac/register",
			wantError:     true,
		},
		{
			name:          "command production cannot import concrete leaf",
			filename:      "cmd/notarius/main.go",
			sourcePackage: "main",
			importPath:    moduleImportPrefix + "almanac/extract/events",
			wantError:     true,
		},
		{
			name:          "unknown production package cannot import concrete leaf",
			filename:      "internal/application/bootstrap.go",
			sourcePackage: "application",
			importPath:    moduleImportPrefix + "almanac/extract/events",
			wantError:     true,
		},
		{
			name:          "unknown production package cannot import registrar",
			filename:      "internal/application/bootstrap.go",
			sourcePackage: "application",
			importPath:    moduleImportPrefix + "almanac/register",
			wantError:     true,
		},
		{
			name:          "unknown test package is not a compatibility root",
			filename:      "internal/application/bootstrap_test.go",
			sourcePackage: "application",
			importPath:    moduleImportPrefix + "almanac/extract/events",
			wantError:     true,
		},
		{
			name:          "framework production cannot import concrete module",
			filename:      "internal/framework/pipeline/runner.go",
			sourcePackage: "pipeline",
			importPath:    moduleImportPrefix + "almanac/extract/events",
			wantError:     true,
		},
		{
			name:          "framework production cannot import generic module",
			filename:      "internal/framework/pipeline/runner.go",
			sourcePackage: "pipeline",
			importPath:    moduleImportPrefix + "generic/normalize/noop",
			wantError:     true,
		},
		{
			name:          "core production cannot import concrete module",
			filename:      "internal/core/source/source.go",
			sourcePackage: "source",
			importPath:    moduleImportPrefix + "almanac",
			wantError:     true,
		},
		{
			name:          "core production cannot import generic module",
			filename:      "internal/core/source/source.go",
			sourcePackage: "source",
			importPath:    moduleImportPrefix + "generic/chunk/units",
			wantError:     true,
		},
		{
			name:          "source production may import another core package",
			filename:      "internal/core/source/source.go",
			sourcePackage: "source",
			importPath:    internalImportPrefix + "artifacts",
		},
		{
			name:          "source production cannot import framework",
			filename:      "internal/core/source/source.go",
			sourcePackage: "source",
			importPath:    internalImportPrefix + "framework/contracts",
			wantError:     true,
		},
		{
			name:          "source production cannot import modules",
			filename:      "internal/core/source/source.go",
			sourcePackage: "source",
			importPath:    moduleImportPrefix + "dnd",
			wantError:     true,
		},
		{
			name:          "chunkplan production may import framework",
			filename:      "internal/framework/chunkplan/store.go",
			sourcePackage: "chunkplan",
			importPath:    internalImportPrefix + "framework/contracts",
		},
		{
			name:          "chunkplan production cannot import modules",
			filename:      "internal/framework/chunkplan/store.go",
			sourcePackage: "chunkplan",
			importPath:    moduleImportPrefix + "dnd",
			wantError:     true,
		},
		{
			name:          "framework test may import module implementation",
			filename:      "internal/framework/pipeline/compatibility_test.go",
			sourcePackage: "pipeline",
			importPath:    moduleImportPrefix + "generic/chunk/units",
		},
		{
			name:          "core test may import module implementation",
			filename:      "internal/core/source/compatibility_test.go",
			sourcePackage: "source",
			importPath:    moduleImportPrefix + "almanac",
		},
		{
			name:          "module production cannot import integration infrastructure",
			filename:      "internal/modules/almanac/register/register.go",
			sourcePackage: "register",
			importPath:    moduleImportPrefix + "integration",
			wantError:     true,
		},
		{
			name:          "non-module production cannot import integration infrastructure",
			filename:      "cmd/notarius/main.go",
			sourcePackage: "main",
			importPath:    moduleImportPrefix + "integration/helpers",
			wantError:     true,
		},
		{
			name:          "test may import integration infrastructure",
			filename:      "internal/cli/compatibility_test.go",
			sourcePackage: "cli",
			importPath:    moduleImportPrefix + "integration",
		},
		{
			name:          "black-box integration test may compose families",
			filename:      "internal/modules/integration/example_test.go",
			sourcePackage: "integration_test",
			importPath:    moduleImportPrefix + "almanac/extract/events",
		},
		{
			name:          "white-box integration test is not exempt",
			filename:      "internal/modules/integration/example_test.go",
			sourcePackage: "integration",
			importPath:    moduleImportPrefix + "almanac/extract/events",
			wantError:     true,
		},
		{
			name:          "integration production file is not exempt",
			filename:      "internal/modules/integration/example.go",
			sourcePackage: "integration",
			importPath:    moduleImportPrefix + "almanac/extract/events",
			wantError:     true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := validateImport(tt.filename, tt.sourcePackage, tt.importPath)
			if tt.wantError && err == nil {
				t.Fatal("validateImport() error = nil, want boundary violation")
			}
			if tt.wantError && (!strings.Contains(err.Error(), tt.filename) || !strings.Contains(err.Error(), tt.importPath)) {
				t.Fatalf("validateImport() error = %q, want importing file and import target", err)
			}
			if !tt.wantError && err != nil {
				t.Fatalf("validateImport() error = %v, want nil", err)
			}
		})
	}
}

func checkImportBoundaries(repositoryRoot string, filename string) error {
	parsed, err := parser.ParseFile(token.NewFileSet(), filename, nil, parser.ImportsOnly)
	if err != nil {
		return fmt.Errorf("parse %s: %w", filename, err)
	}
	relative, err := filepath.Rel(repositoryRoot, filename)
	if err != nil {
		return fmt.Errorf("resolve relative path for %s: %w", filename, err)
	}
	relative = filepath.ToSlash(relative)
	for _, imported := range parsed.Imports {
		importPath, err := strconv.Unquote(imported.Path.Value)
		if err != nil {
			return fmt.Errorf("parse import in %s: %w", relative, err)
		}
		if err := validateImport(relative, parsed.Name.Name, importPath); err != nil {
			return err
		}
	}
	return nil
}

func validateImport(filename string, sourcePackage string, importPath string) error {
	if !strings.HasSuffix(filename, "_test.go") && strings.HasPrefix(filename, "internal/core/source/") && strings.HasPrefix(importPath, internalImportPrefix+"framework/") {
		return importBoundaryViolation(filename, importPath, "core/source production code must not import framework or module implementations")
	}
	target, ok := moduleTargetForImport(importPath)
	if !ok {
		return nil
	}
	isTest := strings.HasSuffix(filename, "_test.go")
	if target.integration && !isTest {
		return importBoundaryViolation(filename, importPath, "module integration infrastructure is not a production dependency target")
	}
	if isIntegrationFile(filename) {
		if isBlackBoxIntegrationTest(filename, sourcePackage) {
			return nil
		}
		return importBoundaryViolation(filename, importPath, "module integration composition is allowed only in black-box tests")
	}
	sourceFamily, sourceRoot, sourceRegistrar := moduleFamilyForFile(filename)
	if sourceFamily != "" {
		if sourceRoot && sourceFamily == target.family && target.child {
			return importBoundaryViolation(filename, importPath, "family root must not import child packages")
		}
		if sourceFamily == target.family {
			return nil
		}
		if sourceFamily == "generic" {
			return importBoundaryViolation(filename, importPath, fmt.Sprintf("generic family must not import concrete family %q", target.family))
		}
		if target.family == "generic" {
			if sourceRegistrar {
				return nil
			}
			return importBoundaryViolation(filename, importPath, fmt.Sprintf("concrete family %q may import generic implementations only from its registrar", sourceFamily))
		}
		return importBoundaryViolation(filename, importPath, fmt.Sprintf("concrete family %q must not import concrete family %q", sourceFamily, target.family))
	}
	if isTest {
		if isCompatibilityTestFile(filename) {
			return nil
		}
		return importBoundaryViolation(filename, importPath, "direct module imports from non-module tests are allowed only in CLI, core, and framework compatibility-test roots")
	}
	if strings.HasPrefix(filename, "internal/core/source/") && strings.HasPrefix(importPath, moduleImportPrefix) {
		return importBoundaryViolation(filename, importPath, "core/source production code must not import framework or module implementations")
	}
	if strings.HasPrefix(filename, "internal/framework/chunkplan/") && strings.HasPrefix(importPath, moduleImportPrefix) {
		return importBoundaryViolation(filename, importPath, "framework/chunkplan production code must not import module implementations")
	}
	if strings.HasPrefix(filename, "internal/framework/") || strings.HasPrefix(filename, "internal/core/") {
		return importBoundaryViolation(filename, importPath, "core and framework production code must not import module implementations")
	}
	if strings.HasPrefix(filename, "internal/cli/") {
		if target.registrar {
			return nil
		}
		return importBoundaryViolation(filename, importPath, "CLI production code may import only exact module family registrar packages")
	}
	return importBoundaryViolation(filename, importPath, "production code outside module families may import modules only from the CLI composition root through exact registrar packages")
}

type moduleImportTarget struct {
	family      string
	child       bool
	registrar   bool
	integration bool
}

func moduleTargetForImport(importPath string) (moduleImportTarget, bool) {
	if !strings.HasPrefix(importPath, moduleImportPrefix) {
		return moduleImportTarget{}, false
	}
	remainder := strings.TrimPrefix(importPath, moduleImportPrefix)
	parts := strings.Split(remainder, "/")
	if len(parts) == 0 || parts[0] == "" {
		return moduleImportTarget{}, false
	}
	return moduleImportTarget{
		family:      parts[0],
		child:       len(parts) > 1,
		registrar:   len(parts) == 2 && parts[1] == "register",
		integration: parts[0] == "integration",
	}, true
}

func importBoundaryViolation(filename string, importPath string, rule string) error {
	return fmt.Errorf("import boundary violation: %s imports %s: %s", filename, importPath, rule)
}

func moduleFamilyForFile(filename string) (family string, root bool, registrar bool) {
	const prefix = "internal/modules/"
	if !strings.HasPrefix(filename, prefix) {
		return "", false, false
	}
	remainder := strings.TrimPrefix(filename, prefix)
	parts := strings.Split(remainder, "/")
	if len(parts) < 2 || parts[0] == "integration" {
		return "", false, false
	}
	return parts[0], len(parts) == 2, len(parts) > 2 && parts[1] == "register"
}

func isIntegrationFile(filename string) bool {
	return strings.HasPrefix(filename, "internal/modules/integration/")
}

func isBlackBoxIntegrationTest(filename string, sourcePackage string) bool {
	return isIntegrationFile(filename) && strings.HasSuffix(filename, "_test.go") && sourcePackage == "integration_test"
}

func isCompatibilityTestFile(filename string) bool {
	if !strings.HasSuffix(filename, "_test.go") {
		return false
	}
	return strings.HasPrefix(filename, "internal/cli/") ||
		strings.HasPrefix(filename, "internal/core/") ||
		strings.HasPrefix(filename, "internal/framework/")
}

func testRepositoryRoot(t *testing.T) string {
	t.Helper()
	_, filename, _, ok := runtime.Caller(0)
	if !ok {
		t.Fatal("resolve import-boundary test location")
	}
	return filepath.Clean(filepath.Join(filepath.Dir(filename), "..", ".."))
}
