package db

import (
	"database/sql"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"testing"

	mysql "github.com/go-sql-driver/mysql"
	_ "modernc.org/sqlite"

	"ymhut-box/server/feedback-mailer/internal/config"
)

func TestOpenMigratesLegacyFeedbackColumns(t *testing.T) {
	dir := t.TempDir()
	storage := filepath.Join(dir, "storage")
	if err := os.MkdirAll(storage, 0o750); err != nil {
		t.Fatal(err)
	}
	dbPath := filepath.Join(storage, "feedback.sqlite")
	conn, err := sql.Open("sqlite", dbPath)
	if err != nil {
		t.Fatal(err)
	}
	_, err = conn.Exec(`CREATE TABLE feedbacks (
		code TEXT PRIMARY KEY,
		received_at TEXT NOT NULL,
		title TEXT NOT NULL,
		type TEXT NOT NULL,
		severity TEXT NOT NULL,
		contact TEXT NOT NULL,
		body TEXT NOT NULL,
		status TEXT NOT NULL,
		package_path TEXT NOT NULL,
		package_sha256 TEXT NOT NULL,
		remote_addr TEXT NOT NULL,
		summary_text TEXT NOT NULL,
		updated_at TEXT NOT NULL
	)`)
	if closeErr := conn.Close(); closeErr != nil {
		t.Fatal(closeErr)
	}
	if err != nil {
		t.Fatal(err)
	}

	cfg := config.Default(dir)
	cfg.StorageDir = storage
	cfg.DatabasePath = dbPath
	store, err := Open(cfg)
	if err != nil {
		t.Fatal(err)
	}
	defer store.Close()

	for _, column := range []string{"public_reply", "included_files", "mail_sent", "encrypted_package_path", "plain_package_sha256", "assignee", "due_at", "sla_level", "source_channel", "risk_score", "resolution"} {
		if !hasColumn(t, store.db, "feedbacks", column) {
			t.Fatalf("expected migrated column %q", column)
		}
	}
	for _, table := range []string{"feedback_comments", "feedback_tags", "audit_logs", "webhook_deliveries"} {
		if !hasTable(t, store.db, table) {
			t.Fatalf("expected migrated table %q", table)
		}
	}
	if mode := store.WALMode(); mode != "wal" {
		t.Fatalf("expected wal mode, got %q", mode)
	}
}

func TestTicketExtensionsRoundTrip(t *testing.T) {
	dir := t.TempDir()
	cfg := config.Default(dir)
	store, err := Open(cfg)
	if err != nil {
		t.Fatal(err)
	}
	defer store.Close()

	record := FeedbackRecord{
		Code:                 "FB-20260606-ABC123",
		ReceivedAt:           Now(),
		Title:                "Crash",
		Type:                 "issue",
		Severity:             "major",
		Contact:              "dev@example.com",
		Body:                 "Steps",
		Status:               "new",
		StatusDetail:         "received",
		PackagePath:          "a.zip",
		EncryptedPackagePath: "a.ymfb",
		PackageSha256:        strings.Repeat("a", 64),
		PlainPackageSha256:   strings.Repeat("b", 64),
		RemoteAddr:           "127.0.0.1",
		SummaryText:          "summary",
		IncludedFiles:        "feedback.json",
		UpdatedAt:            Now(),
		LastActivityAt:       Now(),
		Tags:                 []string{"crash", "UI"},
	}
	if err := store.InsertFeedback(record); err != nil {
		t.Fatal(err)
	}
	if err := store.UpdateFeedback(record.Code, FeedbackUpdate{
		Status:       "investigating",
		Category:     "issue",
		Priority:     "major",
		StatusDetail: "checking",
		Assignee:     "alice",
		SLALevel:     "elevated",
		Note:         "internal",
		PublicReply:  "reply",
		Actor:        "alice",
		Tags:         []string{"crash", "priority"},
	}); err != nil {
		t.Fatal(err)
	}
	if _, err := store.InsertComment(FeedbackComment{FeedbackCode: record.Code, Author: "alice", Body: "comment", Internal: true}); err != nil {
		t.Fatal(err)
	}
	if err := store.InsertAudit(AuditLog{Actor: "alice", Type: "feedback.updated", Target: record.Code, Message: "updated"}); err != nil {
		t.Fatal(err)
	}
	if _, err := store.InsertWebhookDelivery(WebhookDelivery{WebhookName: "ops", Event: "feedback.updated", Status: "pending"}); err != nil {
		t.Fatal(err)
	}

	detail, err := store.GetFeedbackDetail(record.Code)
	if err != nil {
		t.Fatal(err)
	}
	if detail.Assignee != "alice" || detail.SLALevel != "elevated" || len(detail.Comments) != 1 || len(detail.Tags) != 2 {
		t.Fatalf("unexpected detail: %+v", detail)
	}
	page, err := store.ListFeedbacks(1, 20, FeedbackFilters{Assignee: "alice", Tag: "priority", SLA: "elevated"})
	if err != nil {
		t.Fatal(err)
	}
	if page.Total != 1 {
		t.Fatalf("expected filtered ticket, got %+v", page)
	}
}

func TestApplyDatabaseConfigSwitchesSQLitePath(t *testing.T) {
	dir := t.TempDir()
	cfg := config.Default(dir)
	store, err := Open(cfg)
	if err != nil {
		t.Fatal(err)
	}
	defer store.Close()

	next := cfg.Database
	next.Provider = "sqlite"
	next.SQLitePath = filepath.Join(dir, "storage", "next.sqlite")
	if err := store.ApplyDatabaseConfig(next); err != nil {
		t.Fatal(err)
	}
	if store.Status().ActiveProvider != "sqlite" {
		t.Fatalf("expected sqlite active provider, got %+v", store.Status())
	}
	if _, err := os.Stat(next.SQLitePath); err != nil {
		t.Fatalf("expected new sqlite database at %s: %v", next.SQLitePath, err)
	}
	if !hasTable(t, store.DB(), "feedbacks") {
		t.Fatal("expected migrated feedbacks table on switched sqlite database")
	}
}

func TestDatabaseDSNEncodesRemoteCredentials(t *testing.T) {
	dir := t.TempDir()
	sqliteDSN, err := databaseDSN(config.DatabaseConfig{
		Provider:   "sqlite",
		SQLitePath: "storage/feedback.sqlite",
	}, dir)
	if err != nil {
		t.Fatal(err)
	}
	if sqliteDSN != filepath.Join(dir, "storage", "feedback.sqlite") {
		t.Fatalf("relative sqlite path should resolve from service root, got %q", sqliteDSN)
	}

	mysqlDSN, err := databaseDSN(config.DatabaseConfig{
		Provider: "mysql",
		Host:     "db.example.com",
		Port:     3307,
		Name:     "feedback_db",
		User:     "feedback_user",
		Password: "p@ss/word",
	}, dir)
	if err != nil {
		t.Fatal(err)
	}
	parsedMySQL, err := mysql.ParseDSN(mysqlDSN)
	if err != nil {
		t.Fatal(err)
	}
	if parsedMySQL.User != "feedback_user" || parsedMySQL.Passwd != "p@ss/word" || parsedMySQL.DBName != "feedback_db" || parsedMySQL.Addr != "db.example.com:3307" {
		t.Fatalf("mysql DSN did not preserve settings: %+v", parsedMySQL)
	}

	postgresDSN, err := databaseDSN(config.DatabaseConfig{
		Provider: "postgres",
		Host:     "pg.example.com",
		Port:     5433,
		Name:     "feedback/db",
		User:     "feedback:user",
		Password: "p@ss/word",
		SSLMode:  "require",
	}, dir)
	if err != nil {
		t.Fatal(err)
	}
	parsed, err := url.Parse(postgresDSN)
	if err != nil {
		t.Fatal(err)
	}
	if parsed.Scheme != "postgres" || parsed.Host != "pg.example.com:5433" || parsed.Query().Get("sslmode") != "require" {
		t.Fatalf("postgres DSN was not safely formatted: %s", postgresDSN)
	}
	if password, ok := parsed.User.Password(); !ok || password != "p@ss/word" || parsed.User.Username() != "feedback:user" {
		t.Fatalf("postgres credentials were not preserved: %s", postgresDSN)
	}
}

func hasColumn(t *testing.T, conn *sql.DB, table, column string) bool {
	t.Helper()
	rows, err := conn.Query(`PRAGMA table_info(` + table + `)`)
	if err != nil {
		t.Fatal(err)
	}
	defer rows.Close()

	for rows.Next() {
		var cid int
		var name, typ string
		var notNull int
		var dflt sql.NullString
		var pk int
		if err := rows.Scan(&cid, &name, &typ, &notNull, &dflt, &pk); err != nil {
			t.Fatal(err)
		}
		if name == column {
			return true
		}
	}
	return false
}

func hasTable(t *testing.T, conn *sql.DB, table string) bool {
	t.Helper()
	row := conn.QueryRow(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`, table)
	var name string
	if err := row.Scan(&name); err != nil {
		return false
	}
	return name == table
}
