package webui

import (
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"testing"
	"time"

	"gopkg.in/ini.v1"
	"mailgoserver/internal/acmecert"
	"mailgoserver/internal/db"
	"mailgoserver/internal/dkim"
	"mailgoserver/internal/mailstore"
	"mailgoserver/internal/relay"
	"mailgoserver/internal/toolbox"
)

func newTestApp(t *testing.T) *App {
	t.Helper()
	dir := t.TempDir()
	dbPath := filepath.Join(dir, "test.db")
	database, err := db.Open(dbPath)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { database.Close() })

	// Seed one of everything so every page has real data to render.
	domainID, err := database.CreateDomain("example.com")
	if err != nil {
		t.Fatal(err)
	}
	hash, _ := db.HashPassword("testpass123")
	senderID, err := database.CreateSender("test@example.com", hash, domainID, true, true)
	if err != nil {
		t.Fatal(err)
	}
	_, err = database.CreateWhitelistedIP("127.0.0.1", domainID, false)
	if err != nil {
		t.Fatal(err)
	}

	dkimMgr := dkim.New(database, 1024)
	ok, err := dkimMgr.GenerateDKIMKeypair("example.com", "sel1", false)
	if err != nil || !ok {
		t.Fatalf("dkim gen: ok=%v err=%v", ok, err)
	}
	key, err := database.GetActiveDKIMKeyByDomainID(domainID)
	if err != nil || key == nil {
		t.Fatalf("get active dkim key: %v %v", key, err)
	}

	logID, err := database.InsertEmailLog(db.EmailLog{
		MessageID: "abc123@example.com", Timestamp: time.Now(), PeerIP: "127.0.0.1",
		MailFrom: "test@example.com", ToAddress: "rcpt@example.org", Subject: "hi",
		EmailHeaders: "From: test@example.com\nTo: rcpt@example.org", MessageBody: "hello",
		Status: "relayed", DKIMSigned: true, Username: "test@example.com",
	})
	if err != nil {
		t.Fatal(err)
	}
	if err := database.InsertEmailRecipientLog(db.EmailRecipientLog{EmailLogID: logID, Recipient: "rcpt@example.org", RecipientType: "to", Status: "success"}); err != nil {
		t.Fatal(err)
	}
	attFile := filepath.Join(dir, "att.txt")
	os.WriteFile(attFile, []byte("attachment data"), 0o644)
	if err := database.InsertEmailAttachment(db.EmailAttachment{EmailLogID: logID, Filename: "att.txt", ContentType: "text/plain", FilePath: attFile, Size: 15}); err != nil {
		t.Fatal(err)
	}
	if err := database.LogAuthAttempt("sender", "test@example.com", "127.0.0.1", true, "ok"); err != nil {
		t.Fatal(err)
	}

	mstore := mailstore.New(database, mailstore.GenerateDEK(), filepath.Join(dir, "mailstore"))
	mdek := mailstore.GenerateDEK()
	mwrapped, mnonce, err := mstore.WrapDEK(mdek)
	if err != nil {
		t.Fatal(err)
	}
	mboxID, err := database.CreateMailbox("inbox@example.com", hash, domainID, 5*1024*1024*1024, mwrapped, mnonce)
	if err != nil {
		t.Fatal(err)
	}

	cfg := ini.Empty()
	serverSec, _ := cfg.NewSection("Server")
	serverSec.NewKey("smtp_port", "4025")
	serverSec.NewKey("smtp_tls_port", "40465")
	serverSec.NewKey("web_http_port", "5000")
	serverSec.NewKey("web_https_port", "5001")
	serverSec.NewKey("bind_ip", "0.0.0.0")
	serverSec.NewKey("time_zone", "UTC")
	serverSec.NewKey("hostname", "mail.example.com")
	serverSec.NewKey("helo_hostname", "mail.example.com")
	serverSec.NewKey("server_banner", "")
	imapSec, _ := cfg.NewSection("IMAP")
	imapSec.NewKey("imap_port", "1143")
	imapSec.NewKey("imap_tls_port", "1993")
	authSec, _ := cfg.NewSection("Auth")
	authSec.NewKey("enforce_admin_mfa", "false")
	authSec.NewKey("enforce_mailbox_mfa", "false")
	rspamdSec, _ := cfg.NewSection("Rspamd")
	rspamdSec.NewKey("enabled", "false")
	rspamdSec.NewKey("url", "http://127.0.0.1:11333")
	rspamdSec.NewKey("reject_score", "15")
	dbSec, _ := cfg.NewSection("Database")
	dbSec.NewKey("database_url", "sqlite:///server_data/smtp_server.db")
	logSec, _ := cfg.NewSection("Logging")
	logSec.NewKey("log_level", "INFO")
	logSec.NewKey("hide_info_aiosmtpd", "true")
	relaySec, _ := cfg.NewSection("Relay")
	relaySec.NewKey("relay_timeout", "30")
	tlsSec, _ := cfg.NewSection("TLS")
	tlsSec.NewKey("tls_cert_file", "ssl_certs/server.crt")
	tlsSec.NewKey("tls_key_file", "ssl_certs/server.key")
	tlsSec.NewKey("smtp_tls_cert", "custom")
	tlsSec.NewKey("imap_tls_cert", "custom")
	tlsSec.NewKey("web_https_cert", "custom")
	dkimSec, _ := cfg.NewSection("DKIM")
	dkimSec.NewKey("dkim_key_size", "2048")
	dkimSec.NewKey("spf_server_ip", "192.168.1.1")
	attSec, _ := cfg.NewSection("Attachments")
	attSec.NewKey("attachments_path", filepath.Join(dir, "attachments"))
	mailstoreSec, _ := cfg.NewSection("Mailstore")
	mailstoreSec.NewKey("app_password_min_length", "25")
	mailstoreSec.NewKey("spam_reject_score", "5")

	configPath := filepath.Join(dir, "settings.ini")
	cfg.SaveTo(configPath)

	acmeMgr := acmecert.New(cfg, "LetsEncrypt", "dns-01", filepath.Join(dir, "server.crt"), filepath.Join(dir, "server.key"), filepath.Join(dir, "acme"), nil, toolbox.GetLogger("test"))
	acmeHTTPMgr := acmecert.New(cfg, "LetsEncryptHTTP", "http-01", filepath.Join(dir, "server.crt"), filepath.Join(dir, "server.key"), filepath.Join(dir, "acme"), nil, toolbox.GetLogger("test"))
	relayer := relay.New(database, cfg, toolbox.GetLogger("test"))
	relayer.Mailstore = mstore
	appSecret, err := LoadOrCreateAppSecret(filepath.Join(dir, "app_secret.key"))
	if err != nil {
		t.Fatalf("LoadOrCreateAppSecret: %v", err)
	}
	app, err := New(database, dkimMgr, mstore, acmeMgr, acmeHTTPMgr, relayer, cfg, configPath, toolbox.GetLogger("test"), func() bool { return true }, appSecret)
	if err != nil {
		t.Fatalf("New: %v", err)
	}

	_ = senderID
	_ = key
	_ = mboxID
	return app
}

// loginSession creates a fully-verified admin session (no MFA enrolled) and returns
// its cookie, for tests that need to hit routes behind requireAuth.
func loginSession(t *testing.T, app *App) *http.Cookie {
	t.Helper()
	hash, err := db.HashPassword("test-password-123!")
	if err != nil {
		t.Fatal(err)
	}
	userID, err := app.DB.CreateAdminUser("test-admin", hash, false)
	if err != nil {
		t.Fatal(err)
	}
	token, err := app.DB.CreateSession(userID, true, time.Hour)
	if err != nil {
		t.Fatal(err)
	}
	return &http.Cookie{Name: sessionCookieName, Value: token}
}

func TestAllPagesRender(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	cookie := loginSession(t, app)

	domains, _ := app.DB.ListDomains()
	senders, _ := app.DB.ListSenders()
	mailboxes, _ := app.DB.ListMailboxes()
	ips, _ := app.DB.ListWhitelistedIPs()
	keys, _ := app.DB.ListActiveDKIMKeysWithDomain()
	logs, _ := app.DB.ListEmailLogsPage(0, 10)
	if len(domains) == 0 || len(senders) == 0 || len(mailboxes) == 0 || len(ips) == 0 || len(keys) == 0 || len(logs) == 0 {
		t.Fatalf("seed data missing: domains=%d senders=%d mailboxes=%d ips=%d keys=%d logs=%d", len(domains), len(senders), len(mailboxes), len(ips), len(keys), len(logs))
	}

	pagesToCheck := []string{
		"/",
		"/account",
		"/domains", "/domains/add", "/domains/" + itoa(domains[0].ID) + "/edit",
		"/senders", "/senders/add", "/senders/" + itoa(senders[0].ID) + "/edit",
		"/mailboxes", "/mailboxes/add", "/mailboxes/" + itoa(mailboxes[0].ID) + "/edit", "/mailboxes/" + itoa(mailboxes[0].ID) + "/apppasswords", "/mailboxes/" + itoa(mailboxes[0].ID) + "/aliases",
		"/mailboxes/" + itoa(mailboxes[0].ID) + "/lists", "/mailboxes/" + itoa(mailboxes[0].ID) + "/rules",
		"/ips", "/ips/add", "/ips/" + itoa(ips[0].ID) + "/edit",
		"/dkim", "/dkim/" + itoa(keys[0].ID) + "/edit",
		"/logs", "/logs?type=emails", "/logs?type=auth",
		"/logs?type=auth&auth_category=admin", "/logs?type=auth&auth_category=webmail", "/logs?type=auth&auth_category=mailserver",
		"/settings",
		"/letsencrypt",
		"/blacklist",
		"/msg/content/" + itoa(logs[0].ID),
		"/admins", "/admins/add",
	}

	for _, path := range pagesToCheck {
		req := httptest.NewRequest(http.MethodGet, Prefix+path, nil)
		req.AddCookie(cookie)
		rec := httptest.NewRecorder()
		mux.ServeHTTP(rec, req)
		if rec.Code != http.StatusOK {
			t.Errorf("GET %s: status %d, body: %s", path, rec.Code, rec.Body.String())
		}
	}
}

func TestUnauthenticatedRequestsRedirectToLogin(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()

	req := httptest.NewRequest(http.MethodGet, Prefix+"/domains", nil)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	if rec.Code != http.StatusFound {
		t.Fatalf("expected redirect to login, got status %d", rec.Code)
	}
	loc := rec.Header().Get("Location")
	if !strings.HasPrefix(loc, Prefix+"/login") {
		t.Fatalf("expected redirect to login page, got %q", loc)
	}
}

func TestFirstLoginForcedBeforeDashboard(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()

	hash, err := db.HashPassword("Password123!")
	if err != nil {
		t.Fatal(err)
	}
	userID, err := app.DB.CreateAdminUser("admin", hash, true) // must_change_password
	if err != nil {
		t.Fatal(err)
	}
	token, err := app.DB.CreateSession(userID, true, time.Hour)
	if err != nil {
		t.Fatal(err)
	}
	cookie := &http.Cookie{Name: sessionCookieName, Value: token}

	req := httptest.NewRequest(http.MethodGet, Prefix+"/domains", nil)
	req.AddCookie(cookie)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	if rec.Code != http.StatusFound || rec.Header().Get("Location") != Prefix+"/first-login" {
		t.Fatalf("expected redirect to /first-login, got status %d location %q", rec.Code, rec.Header().Get("Location"))
	}
}

func itoa(id int64) string {
	return strconv.FormatInt(id, 10)
}
