package catalogassignmentidentity

import (
	"context"
	"database/sql"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"reflect"
	"runtime"
	"sort"
	"strconv"
	"strings"
	"testing"

	catalogassignmentidentityv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/catalogassignmentidentityv1"
)

var (
	_ func(*sql.DB) (*Store, error)                                                           = New
	_ func(context.Context, *sql.DB) error                                                    = EnsureSchema
	_ func() []string                                                                         = SchemaStatementsForTest
	_ func(*Store, context.Context, catalogassignmentidentityv1.View) (string, error)         = (*Store).PutExact
	_ func(*Store, context.Context, string, string) (catalogassignmentidentityv1.View, error) = (*Store).ReadExact
)

func TestStoreSurfaceIsExactLookupAndAppendOnly(t *testing.T) {
	storeType := reflect.TypeOf(Store{})
	if storeType.NumField() != 1 {
		t.Fatalf("Store has %d fields, want only the database handle", storeType.NumField())
	}
	dbField := storeType.Field(0)
	if dbField.Name != "db" ||
		dbField.Type != reflect.TypeOf((*sql.DB)(nil)) ||
		dbField.PkgPath == "" {
		t.Fatalf("Store field = %#v, want one unexported db *sql.DB", dbField)
	}

	methodType := reflect.TypeOf((*Store)(nil))
	wantMethods := map[string]reflect.Type{
		"PutExact": reflect.TypeOf(
			(func(*Store, context.Context, catalogassignmentidentityv1.View) (string, error))(nil),
		),
		"ReadExact": reflect.TypeOf(
			(func(*Store, context.Context, string, string) (catalogassignmentidentityv1.View, error))(nil),
		),
	}
	if methodType.NumMethod() != len(wantMethods) {
		t.Fatalf("Store exposes %d methods, want only PutExact and ReadExact", methodType.NumMethod())
	}
	for name, wantType := range wantMethods {
		method, ok := methodType.MethodByName(name)
		if !ok {
			t.Fatalf("Store is missing %s", name)
		}
		if method.Type != wantType {
			t.Fatalf("%s has type %v, want %v", name, method.Type, wantType)
		}
	}

	if ErrNotFound == nil || ErrCorrupt == nil {
		t.Fatal("exact reads require non-nil not-found and corruption sentinels")
	}
	if ErrNotFound == ErrCorrupt {
		t.Fatal("not-found and corruption sentinels must remain distinct")
	}
	if store, err := New(nil); err == nil || store != nil {
		t.Fatalf("New(nil) = (%v, %v), want (nil, error)", store, err)
	}
}

func TestPackageExportsOnlyTheImmutableStoreContract(t *testing.T) {
	want := []string{
		"func:EnsureSchema",
		"func:New",
		"func:SchemaStatementsForTest",
		"method:PutExact",
		"method:ReadExact",
		"type:Store",
		"var:ErrCorrupt",
		"var:ErrNotFound",
	}
	got := assignmentIdentityExportedDeclarations(t)
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("exported package surface = %v, want %v", got, want)
	}
}

func TestPackageImportsOnlyTheNeutralContractAndStandardLibrary(t *testing.T) {
	allowedExternal := map[string]struct{}{
		"github.com/pingcap-inc/nexuscrm-contract/pkg/catalogassignmentidentityv1": {},
	}
	for _, path := range assignmentIdentityProductionFiles(t) {
		parsed, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ImportsOnly)
		if err != nil {
			t.Fatalf("parse imports %s: %v", path, err)
		}
		for _, spec := range parsed.Imports {
			importPath, err := strconv.Unquote(spec.Path.Value)
			if err != nil {
				t.Fatalf("unquote import %s in %s: %v", spec.Path.Value, path, err)
			}
			if strings.HasPrefix(
				importPath,
				"github.com/pingcap-inc/nexuscrm-controlplane/",
			) {
				t.Errorf("lower assignment-identity store %s imports upper controlplane package %s", path, importPath)
				continue
			}
			firstPart := strings.Split(importPath, "/")[0]
			if !strings.Contains(firstPart, ".") {
				continue
			}
			if _, ok := allowedExternal[importPath]; !ok {
				t.Errorf("assignment-identity store %s imports non-contract dependency %s", path, importPath)
			}
		}
	}
}

func TestOnlyCatalogPublicationOwnsTheStore(t *testing.T) {
	const storeImport = "github.com/pingcap-inc/nexuscrm-controlplane/internal/catalogassignmentidentity"
	repoRoot := assignmentIdentityRepoRoot(t)
	var consumers []string

	err := filepath.WalkDir(repoRoot, func(path string, entry os.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if entry.IsDir() {
			switch entry.Name() {
			case ".git", "testdata", "vendor":
				return filepath.SkipDir
			default:
				return nil
			}
		}
		if filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") {
			return nil
		}

		fset := token.NewFileSet()
		parsed, err := parser.ParseFile(fset, path, nil, 0)
		if err != nil {
			return err
		}
		for _, spec := range parsed.Imports {
			importPath, err := strconv.Unquote(spec.Path.Value)
			if err != nil {
				return err
			}
			if importPath != storeImport {
				continue
			}

			relative, err := filepath.Rel(repoRoot, path)
			if err != nil {
				return err
			}
			relative = filepath.ToSlash(relative)
			consumers = append(consumers, relative)

			alias := "catalogassignmentidentity"
			if spec.Name != nil {
				alias = spec.Name.Name
			}
			if alias == "." || alias == "_" {
				t.Errorf("%s must use a named assignment-identity import", relative)
				continue
			}
			var selectors []string
			ast.Inspect(parsed, func(node ast.Node) bool {
				selector, ok := node.(*ast.SelectorExpr)
				if !ok {
					return true
				}
				identifier, ok := selector.X.(*ast.Ident)
				if ok && identifier.Name == alias {
					selectors = append(selectors, selector.Sel.Name)
				}
				return true
			})
			sort.Strings(selectors)
			wantSelectorsByConsumer := map[string][]string{
				"catalogpublication/assignment_identity.go": {"New"},
				"catalogpublication/schema.go":              {"EnsureSchema", "SchemaStatementsForTest"},
			}
			wantSelectors, ok := wantSelectorsByConsumer[relative]
			if !ok {
				t.Errorf("%s must not consume the assignment-identity store", relative)
				continue
			}
			if !reflect.DeepEqual(selectors, wantSelectors) {
				t.Errorf("%s uses assignment-identity store selectors %v, want %v", relative, selectors, wantSelectors)
			}
		}
		return nil
	})
	if err != nil {
		t.Fatal(err)
	}
	sort.Strings(consumers)
	if want := []string{"catalogpublication/assignment_identity.go", "catalogpublication/schema.go"}; !reflect.DeepEqual(consumers, want) {
		t.Fatalf("production assignment-identity store consumers = %v, want %v", consumers, want)
	}
}

func assignmentIdentityExportedDeclarations(t *testing.T) []string {
	t.Helper()
	var result []string
	for _, path := range assignmentIdentityProductionFiles(t) {
		parsed, err := parser.ParseFile(token.NewFileSet(), path, nil, 0)
		if err != nil {
			t.Fatalf("parse %s: %v", path, err)
		}
		for _, declaration := range parsed.Decls {
			switch declaration := declaration.(type) {
			case *ast.FuncDecl:
				if !ast.IsExported(declaration.Name.Name) {
					continue
				}
				kind := "func"
				if declaration.Recv != nil {
					kind = "method"
				}
				result = append(result, kind+":"+declaration.Name.Name)
			case *ast.GenDecl:
				for _, spec := range declaration.Specs {
					switch spec := spec.(type) {
					case *ast.TypeSpec:
						if ast.IsExported(spec.Name.Name) {
							result = append(result, "type:"+spec.Name.Name)
						}
					case *ast.ValueSpec:
						for _, name := range spec.Names {
							if ast.IsExported(name.Name) {
								result = append(
									result,
									strings.ToLower(declaration.Tok.String())+":"+name.Name,
								)
							}
						}
					}
				}
			}
		}
	}
	sort.Strings(result)
	return result
}

func assignmentIdentityProductionFiles(t *testing.T) []string {
	t.Helper()
	packageDir := filepath.Join(assignmentIdentityRepoRoot(t), "internal", "catalogassignmentidentity")
	entries, err := os.ReadDir(packageDir)
	if err != nil {
		t.Fatal(err)
	}
	var result []string
	for _, entry := range entries {
		if entry.IsDir() ||
			filepath.Ext(entry.Name()) != ".go" ||
			strings.HasSuffix(entry.Name(), "_test.go") {
			continue
		}
		result = append(result, filepath.Join(packageDir, entry.Name()))
	}
	sort.Strings(result)
	return result
}

func assignmentIdentityRepoRoot(t *testing.T) string {
	t.Helper()
	_, filename, _, ok := runtime.Caller(0)
	if !ok {
		t.Fatal("resolve assignment-identity source path")
	}
	return filepath.Dir(filepath.Dir(filepath.Dir(filename)))
}
