// Package mysqlschema creates isolated MySQL schemas for service integration tests.
package mysqlschema

import (
	"database/sql"
	"fmt"
	"hash/crc32"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
	"time"

	mysqldriver "github.com/go-sql-driver/mysql"
)

// Config describes how to create one temporary schema from a service initdb script.
type Config struct {
	EnvVar         string
	InitDBPath     string
	DatabasePrefix string
}

// Schema owns one temporary MySQL database initialized from production DDL.
type Schema struct {
	DSN string
	DB  *sql.DB

	t        testing.TB
	adminDB  *sql.DB
	database string
}

// New creates an isolated schema and registers cleanup with t.Cleanup.
func New(t testing.TB, cfg Config) *Schema {
	t.Helper()

	if strings.TrimSpace(cfg.EnvVar) == "" {
		cfg.EnvVar = "MYSQL_TEST_DSN"
	}
	baseDSN := os.Getenv(cfg.EnvVar)
	if strings.TrimSpace(baseDSN) == "" {
		t.Skipf("set %s to run MySQL-backed tests", cfg.EnvVar)
	}
	if strings.TrimSpace(cfg.InitDBPath) == "" {
		t.Fatalf("mysqlschema InitDBPath is required")
	}
	if strings.TrimSpace(cfg.DatabasePrefix) == "" {
		cfg.DatabasePrefix = "hy_test"
	}

	dsnCfg, err := mysqldriver.ParseDSN(baseDSN)
	if err != nil {
		t.Fatalf("parse %s failed: %v", cfg.EnvVar, err)
	}
	database := testDatabaseName(cfg.DatabasePrefix, t.Name())

	adminCfg := dsnCfg.Clone()
	adminCfg.DBName = ""
	adminDB, err := sql.Open("mysql", adminCfg.FormatDSN())
	if err != nil {
		t.Fatalf("open mysql admin connection failed: %v", err)
	}
	if _, err := adminDB.Exec("CREATE DATABASE " + quoteIdentifier(database) + " DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); err != nil {
		_ = adminDB.Close()
		t.Fatalf("create test database failed: %v", err)
	}

	testCfg := dsnCfg.Clone()
	testCfg.DBName = database
	testDSN := testCfg.FormatDSN()
	rawDB, err := sql.Open("mysql", testDSN)
	if err != nil {
		_ = adminDB.Close()
		t.Fatalf("open mysql test database failed: %v", err)
	}
	initSchema(t, rawDB, cfg.InitDBPath)

	schema := &Schema{
		DSN:      testDSN,
		DB:       rawDB,
		t:        t,
		adminDB:  adminDB,
		database: database,
	}
	t.Cleanup(schema.Close)
	return schema
}

// Close closes connections and drops the temporary database.
func (s *Schema) Close() {
	s.t.Helper()

	if s.DB != nil {
		_ = s.DB.Close()
	}
	if s.adminDB != nil && s.database != "" {
		if _, err := s.adminDB.Exec("DROP DATABASE IF EXISTS " + quoteIdentifier(s.database)); err != nil {
			s.t.Fatalf("drop test database %s failed: %v", s.database, err)
		}
		_ = s.adminDB.Close()
	}
}

// InitDBPath resolves a service initdb file relative to the caller source file.
func InitDBPath(t testing.TB, callerFile string, relativePath ...string) string {
	t.Helper()

	parts := append([]string{filepath.Dir(callerFile)}, relativePath...)
	return filepath.Clean(filepath.Join(parts...))
}

// CallerFile returns the current caller file for compact per-service test helpers.
func CallerFile(t testing.TB, skip int) string {
	t.Helper()

	_, file, _, ok := runtime.Caller(skip)
	if !ok {
		t.Fatalf("runtime caller unavailable")
	}
	return file
}

func initSchema(t testing.TB, db *sql.DB, initDBPath string) {
	t.Helper()

	script, err := os.ReadFile(initDBPath)
	if err != nil {
		t.Fatalf("read initdb %s failed: %v", initDBPath, err)
	}
	for statement := range strings.SplitSeq(stripSQLLineComments(string(script)), ";") {
		statement = strings.TrimSpace(statement)
		if statement == "" {
			continue
		}
		upper := strings.ToUpper(statement)
		if strings.HasPrefix(upper, "CREATE DATABASE") || strings.HasPrefix(upper, "USE ") {
			continue
		}
		if _, err := db.Exec(statement); err != nil {
			t.Fatalf("exec initdb statement failed: %v\nstatement:\n%s", err, statement)
		}
	}
}

func stripSQLLineComments(script string) string {
	lines := strings.Split(script, "\n")
	kept := make([]string, 0, len(lines))
	for _, line := range lines {
		if strings.HasPrefix(strings.TrimSpace(line), "--") {
			continue
		}
		kept = append(kept, line)
	}
	return strings.Join(kept, "\n")
}

func testDatabaseName(prefix string, testName string) string {
	checksum := crc32.ChecksumIEEE([]byte(testName))
	return fmt.Sprintf("%s_%08x_%d", sanitizePrefix(prefix), checksum, time.Now().UnixNano())
}

func sanitizePrefix(prefix string) string {
	prefix = strings.ToLower(strings.TrimSpace(prefix))
	var builder strings.Builder
	for _, char := range prefix {
		if (char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char == '_' {
			builder.WriteRune(char)
		}
	}
	if builder.Len() == 0 {
		return "hy_test"
	}
	return builder.String()
}

func quoteIdentifier(identifier string) string {
	return "`" + strings.ReplaceAll(identifier, "`", "``") + "`"
}
