package webui

import (
	"net/http"
	"net/http/httptest"
	"net/url"
	"strconv"
	"strings"
	"testing"

	"mailgoserver/internal/db"
)

// scopedLogin creates a domain-scoped (non-global) admin with access to exactly
// domainIDs and returns its session cookie. CreateScopedAdminUser always sets
// must_change_password (matching the real "you can't keep an admin-picked initial
// password" flow), so this clears it the same way completing /first-login would —
// otherwise every protected route redirects to /first-login before the scoping logic
// these tests exercise ever runs.
func scopedLogin(t *testing.T, app *App, username string, domainIDs []int64) *http.Cookie {
	t.Helper()
	hash, err := db.HashPassword("scoped-password-123!")
	if err != nil {
		t.Fatal(err)
	}
	userID, err := app.DB.CreateScopedAdminUser(username, hash, 0, domainIDs)
	if err != nil {
		t.Fatal(err)
	}
	if err := app.DB.UpdateAdminCredentials(userID, username, hash); err != nil {
		t.Fatal(err)
	}
	token, err := app.DB.CreateSession(userID, true, sessionTTL)
	if err != nil {
		t.Fatal(err)
	}
	return &http.Cookie{Name: sessionCookieName, Value: token}
}

// setupTwoTenants seeds two separate domains, each with its own sender, whitelisted
// IP, and DKIM key, and returns everything needed to test cross-tenant isolation.
func setupTwoTenants(t *testing.T, app *App) (domainA, domainB db.Domain, senderA, senderB *db.Sender) {
	t.Helper()
	aID, err := app.DB.CreateDomain("tenant-a.example")
	if err != nil {
		t.Fatal(err)
	}
	bID, err := app.DB.CreateDomain("tenant-b.example")
	if err != nil {
		t.Fatal(err)
	}
	da, _ := app.DB.GetDomainByID(aID)
	db_, _ := app.DB.GetDomainByID(bID)

	hash, _ := db.HashPassword("password123")
	saID, err := app.DB.CreateSender("alice@tenant-a.example", hash, aID, false, false)
	if err != nil {
		t.Fatal(err)
	}
	sbID, err := app.DB.CreateSender("bob@tenant-b.example", hash, bID, false, false)
	if err != nil {
		t.Fatal(err)
	}
	sa, _ := app.DB.GetSenderByID(saID)
	sb, _ := app.DB.GetSenderByID(sbID)

	if _, err := app.DKIM.GenerateDKIMKeypair("tenant-a.example", "", false); err != nil {
		t.Fatal(err)
	}
	if _, err := app.DKIM.GenerateDKIMKeypair("tenant-b.example", "", false); err != nil {
		t.Fatal(err)
	}

	return *da, *db_, sa, sb
}

func TestScopedAdminOnlySeesOwnDomainInList(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domainA, domainB, _, _ := setupTwoTenants(t, app)

	cookie := scopedLogin(t, app, "tenant-a-admin", []int64{domainA.ID})

	req := httptest.NewRequest(http.MethodGet, Prefix+"/domains", nil)
	req.AddCookie(cookie)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)

	body := rec.Body.String()
	if !strings.Contains(body, domainA.DomainName) {
		t.Error("scoped admin's own domain should appear in the domains list")
	}
	if strings.Contains(body, domainB.DomainName) {
		t.Error("scoped admin must NOT see a domain outside their assignment")
	}
}

func TestScopedAdminCannotAccessOtherTenantSenderByID(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domainA, _, _, senderB := setupTwoTenants(t, app)

	cookie := scopedLogin(t, app, "tenant-a-admin", []int64{domainA.ID})

	// Direct URL access to a sender belonging to a domain they don't manage.
	req := httptest.NewRequest(http.MethodGet, Prefix+"/senders/"+strconv.FormatInt(senderB.ID, 10)+"/edit", nil)
	req.AddCookie(cookie)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	if rec.Code != http.StatusNotFound {
		t.Fatalf("expected 404 for a sender outside scope, got %d", rec.Code)
	}

	// Same for the mutating route — must not be able to disable it either.
	form := url.Values{}
	req2 := httptest.NewRequest(http.MethodPost, Prefix+"/senders/"+strconv.FormatInt(senderB.ID, 10)+"/delete", strings.NewReader(form.Encode()))
	req2.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req2.AddCookie(cookie)
	rec2 := httptest.NewRecorder()
	mux.ServeHTTP(rec2, req2)
	if rec2.Code != http.StatusNotFound {
		t.Fatalf("expected 404 disabling a sender outside scope, got %d", rec2.Code)
	}
	stillActive, err := app.DB.GetSenderByID(senderB.ID)
	if err != nil || stillActive == nil || !stillActive.IsActive {
		t.Fatal("sender outside scope must not have been modified")
	}
}

func TestScopedAdminCannotCreateSenderOnUnownedDomain(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domainA, domainB, _, _ := setupTwoTenants(t, app)

	cookie := scopedLogin(t, app, "tenant-a-admin", []int64{domainA.ID})

	form := url.Values{
		"local_part": {"mallory"},
		"domain_id":  {strconv.FormatInt(domainB.ID, 10)}, // not theirs
		"password":   {"password123"},
	}
	req := httptest.NewRequest(http.MethodPost, Prefix+"/senders/add", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.AddCookie(cookie)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	if rec.Code != http.StatusNotFound {
		t.Fatalf("expected 404 creating a sender on an unowned domain, got %d", rec.Code)
	}
	if s, _ := app.DB.GetSenderByEmail("mallory@" + domainB.DomainName); s != nil {
		t.Fatal("sender must not have been created on a domain outside the admin's scope")
	}
}

func TestNewDomainAutoGrantedToScopedCreator(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domainA, _, _, _ := setupTwoTenants(t, app)
	cookie := scopedLogin(t, app, "tenant-a-admin", []int64{domainA.ID})
	userID, err := app.DB.GetAdminUserByUsername("tenant-a-admin")
	if err != nil || userID == nil {
		t.Fatal("expected the scoped admin to exist")
	}

	form := url.Values{"domain_name": {"brand-new.example"}}
	req := httptest.NewRequest(http.MethodPost, Prefix+"/domains/add", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.AddCookie(cookie)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	if rec.Code != http.StatusFound {
		t.Fatalf("expected redirect after creating domain, got %d: %s", rec.Code, rec.Body.String())
	}

	newDomain, err := app.DB.GetDomainByNameExact("brand-new.example")
	if err != nil || newDomain == nil {
		t.Fatalf("expected domain to be created: %v", err)
	}
	ids, err := app.DB.AccessibleDomainIDs(userID.ID)
	if err != nil {
		t.Fatal(err)
	}
	found := false
	for _, id := range ids {
		if id == newDomain.ID {
			found = true
		}
	}
	if !found {
		t.Fatal("a scoped admin who creates a domain must automatically get access to it")
	}
}

func TestScopedAdminCannotDelegateUnownedDomain(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domainA, domainB, _, _ := setupTwoTenants(t, app)

	cookie := scopedLogin(t, app, "tenant-a-admin", []int64{domainA.ID})

	form := url.Values{
		"username":   {"sub-admin"},
		"password":   {"sub-admin-password-1!"},
		"domain_ids": {strconv.FormatInt(domainA.ID, 10), strconv.FormatInt(domainB.ID, 10)}, // B isn't theirs
	}
	req := httptest.NewRequest(http.MethodPost, Prefix+"/admins/add", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.AddCookie(cookie)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	if rec.Code != http.StatusFound {
		t.Fatalf("expected redirect (with an error flash), got %d", rec.Code)
	}
	if u, _ := app.DB.GetAdminUserByUsername("sub-admin"); u != nil {
		t.Fatal("admin creation must be rejected outright when it tries to delegate a domain outside the creator's own scope")
	}
}

func TestScopedAdminCanDelegateOwnedDomainAndManageResultingAdmin(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domainA, _, _, _ := setupTwoTenants(t, app)

	cookie := scopedLogin(t, app, "tenant-a-admin", []int64{domainA.ID})

	form := url.Values{
		"username":   {"sub-admin"},
		"password":   {"sub-admin-password-1!"},
		"domain_ids": {strconv.FormatInt(domainA.ID, 10)},
	}
	req := httptest.NewRequest(http.MethodPost, Prefix+"/admins/add", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.AddCookie(cookie)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	if rec.Code != http.StatusFound {
		t.Fatalf("expected redirect after delegating an owned domain, got %d: %s", rec.Code, rec.Body.String())
	}
	subAdmin, err := app.DB.GetAdminUserByUsername("sub-admin")
	if err != nil || subAdmin == nil {
		t.Fatalf("expected sub-admin to be created: %v", err)
	}

	// The delegating admin must be able to see and manage the new sub-admin, per the
	// "any admin within scope, not just ones I personally created" rule.
	listReq := httptest.NewRequest(http.MethodGet, Prefix+"/admins", nil)
	listReq.AddCookie(cookie)
	listRec := httptest.NewRecorder()
	mux.ServeHTTP(listRec, listReq)
	if !strings.Contains(listRec.Body.String(), "sub-admin") {
		t.Fatal("delegating admin should see the newly created sub-admin in their admin list")
	}
}

func TestGlobalAdminSeesEverything(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domainA, domainB, _, _ := setupTwoTenants(t, app)
	cookie := loginSession(t, app) // global admin

	req := httptest.NewRequest(http.MethodGet, Prefix+"/domains", nil)
	req.AddCookie(cookie)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	body := rec.Body.String()
	if !strings.Contains(body, domainA.DomainName) || !strings.Contains(body, domainB.DomainName) {
		t.Fatal("global admin must see every domain regardless of scoped assignments")
	}
}
