package seals_test

import (
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

// TestRuntimeDatabaseSQLImportsStayLedgered is the B6 runtime database/sql seal for
// the salesforce-crm dialect (github.com/nexuscrm/dialect-sf-crm). It is a
// FILE-LEVEL import scan — not a dependency-closure check — because the rule it
// enforces ("exactly ONE runtime file may import database/sql") cannot be
// expressed by `go list -deps`: the whole package transitively imports
// database/sql through that one file, and a closure check would also trip on the
// standalone CLI under tools/. This test parses imports only (no build required),
// so it stands independently of toolchain / shared-replace flakiness.
//
// This seal lives in its own stdlib-only `seals` package precisely so app-packs
// PR CI can run it hermetically without resolving `shared` (the standalone
// app-packs checkout has no `replace ../../../shared` target); NexusCRM go-ci
// also runs it via `go test ./...`.
//
// # THE LEDGER
//
// Allowed runtime exception (exactly one):
//
//	content_distribution_resolver.go
//
// Reason: it performs a Salesforce-schema-specific lookup over ContentDistribution
// / IdBody / _System_File (mapping an SF ContentDistribution source reference to a
// local _System_File id via a raw SQL query). That schema knowledge is
// irreducibly Salesforce-dialect, so the SQL legitimately belongs in L1 (this
// dialect), not in the neutral backend.
//
// Auth boundary: this raw query ONLY maps an SF source reference -> local file id.
// Authorization is NOT performed here; it remains neutral downstream via
// Files.ResolveAuthorizedRecordAttachmentSource (see content_distribution_decorator.go),
// which enforces access control on the resolved attachment regardless of how the
// source reference was looked up. The database/sql import therefore does not leak
// any authorization decision into the dialect.
//
// Growth rule: exactly this one file. No additional runtime database/sql imports
// may appear anywhere in this module's runtime code. Adding a new runtime
// importer is a hard error here (assertion (a)) — the fix is to remove the
// import or route through a neutral port, NOT to extend this ledger.
//
// tools/workflowalert is a standalone CLI (package main) — a one-off MySQL
// migration tool. Its database/sql usage is CLI-only and is explicitly OUTSIDE
// this runtime seal: the scan skips the TOP-LEVEL tools/ tree by its exact
// dialect-root-relative path (not by base name), so a same-named NESTED directory
// (e.g. wire/tools) is still scanned and cannot become an escape hatch. The CLI
// is NOT runtime dialect code, NOT a precedent for runtime database/sql, and NOT
// a runtime escape hatch. Runtime dialect code may not point at it to justify a
// new database/sql import.
//
// Future optional improvement (NOT part of B6): a neutral read-only row port
// (e.g. contract.RowQuerier) could let content_distribution_resolver.go run its
// query without importing database/sql at all, removing even this one exception.
// That hardening is deliberately deferred and is out of scope for B6.
//
// Like NexusCRM's TestServiceDataLiteralsStayLedgered, this ratchet is
// BIDIRECTIONAL: it blocks new un-ledgered importers (a) AND blocks a stale
// ledger that names a file which no longer imports database/sql (b).
func TestRuntimeDatabaseSQLImportsStayLedgered(t *testing.T) {
	// The test runs with CWD = the seals/ package dir; the dialect runtime root
	// is therefore the parent directory.
	const dialectRoot = ".."

	// The single ledgered runtime exception, keyed by dialect-root-relative path.
	allowed := map[string]bool{
		"content_distribution_resolver.go": true,
	}

	// Intentional top-level exclusions, matched by EXACT dialect-root-relative
	// path (NOT base name). A base-name skip would also exclude a same-named
	// NESTED directory (e.g. wire/tools), which would be an escape hatch: the
	// invariant is "no runtime database/sql import ANYWHERE in runtime code".
	rootSkip := map[string]bool{
		"tools":  true, // standalone CLI migration tool (package main), CLI-only — top-level tree only.
		"seals":  true, // this seal's own stdlib-only package — top-level only; do not scan itself.
		"vendor": true, // module vendor tree (special at the module root only).
	}

	found := map[string]bool{}

	err := filepath.Walk(dialectRoot, func(path string, info os.FileInfo, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if info.IsDir() {
			if path == dialectRoot {
				return nil
			}
			rel, relErr := filepath.Rel(dialectRoot, path)
			if relErr != nil {
				return relErr
			}
			// Top-level exclusions only — a same-named nested dir is still scanned.
			if rootSkip[filepath.ToSlash(rel)] {
				return filepath.SkipDir
			}
			// Directories Go itself never treats as buildable source, at ANY depth:
			// testdata and dot/underscore-prefixed dirs (e.g. .git). These hold no
			// runtime code, so skipping them by base name is not an escape hatch.
			base := info.Name()
			if base == "testdata" || strings.HasPrefix(base, ".") || strings.HasPrefix(base, "_") {
				return filepath.SkipDir
			}
			return nil
		}
		// Runtime .go files only: skip non-Go and any *_test.go (test-only deps
		// such as go-sqlmock are not runtime imports).
		if filepath.Ext(path) != ".go" {
			return nil
		}
		if strings.HasSuffix(info.Name(), "_test.go") {
			return nil
		}
		f, perr := parser.ParseFile(token.NewFileSet(), path, nil, parser.ImportsOnly)
		if perr != nil {
			return perr
		}
		for _, imp := range f.Imports {
			if imp.Path != nil && imp.Path.Value == `"database/sql"` {
				// Normalize to a dialect-root-relative slash path (e.g. "wire/x.go").
				rel, relErr := filepath.Rel(dialectRoot, path)
				if relErr != nil {
					return relErr
				}
				found[filepath.ToSlash(rel)] = true
				break
			}
		}
		return nil
	})
	if err != nil {
		t.Fatal(err)
	}

	// Assertion (a) — block growth: any found importer not in the ledger is an
	// un-ledgered runtime database/sql import.
	for f := range found {
		if !allowed[f] {
			t.Errorf("un-ledgered runtime database/sql import in %q: runtime dialect code may not import database/sql (only content_distribution_resolver.go is permitted); remove the import or route through a neutral port", f)
		}
	}

	// Assertion (b) — block stale ledger: any allowed entry not present in the
	// found set means the ledger names a file that no longer imports database/sql.
	for f := range allowed {
		if !found[f] {
			t.Errorf("stale ledger entry %q: this file no longer imports database/sql; update the allowed set in seals/runtime_database_sql_seal_test.go", f)
		}
	}
}
