package notify

import (
	"context"
	"errors"
	"log/slog"
	"os"
	"testing"
	"time"

	"github.com/orchard9/rdev/internal/domain"
	"github.com/orchard9/rdev/internal/port"
)

// --- mock implementations ---

// mockAdminClient is a controllable implementation of notifyAdminAPI.
type mockAdminClient struct {
	// accounts simulates the account registry
	accounts []accountResponse

	// Configurable errors per operation
	createHostErr        error
	deleteHostErr        error
	createProviderErr    error
	createFromAddressErr error
	createAccountErr     error
	createSendKeyErr     error
	grantHostAccessErr   error
	deleteAccountErr     error
	listAccountsErr      error

	// Call counters
	createHostCalls        int
	deleteHostCalls        int
	createProviderCalls    int
	createFromAddressCalls int
	createAccountCalls     int
	createSendKeyCalls     int
	grantHostAccessCalls   int
	deleteAccountCalls     int
}

func (m *mockAdminClient) createHost(_ context.Context, _, _ string) error {
	m.createHostCalls++
	return m.createHostErr
}

func (m *mockAdminClient) deleteHost(_ context.Context, _ string) error {
	m.deleteHostCalls++
	return m.deleteHostErr
}

func (m *mockAdminClient) createProvider(_ context.Context, _, _ string, _ map[string]string, _, _, _ int) error {
	m.createProviderCalls++
	return m.createProviderErr
}

func (m *mockAdminClient) createFromAddress(_ context.Context, _, _, _ string) error {
	m.createFromAddressCalls++
	return m.createFromAddressErr
}

func (m *mockAdminClient) createAccount(_ context.Context, name string) (*accountResponse, error) {
	m.createAccountCalls++
	if m.createAccountErr != nil {
		return nil, m.createAccountErr
	}
	acct := &accountResponse{ID: "acct-" + name, Name: name, CreatedAt: time.Now()}
	m.accounts = append(m.accounts, *acct)
	return acct, nil
}

func (m *mockAdminClient) createSendKey(_ context.Context, accountID, name string) (*apiKeyResponse, error) {
	m.createSendKeyCalls++
	if m.createSendKeyErr != nil {
		return nil, m.createSendKeyErr
	}
	return &apiKeyResponse{
		ID:        1,
		Key:       "notify_send_test_key",
		KeyType:   "send",
		AccountID: accountID,
		Name:      name,
	}, nil
}

func (m *mockAdminClient) grantHostAccess(_ context.Context, _, _ string) error {
	m.grantHostAccessCalls++
	return m.grantHostAccessErr
}

func (m *mockAdminClient) deleteAccount(_ context.Context, _ string) error {
	m.deleteAccountCalls++
	return m.deleteAccountErr
}

func (m *mockAdminClient) listAccounts(_ context.Context) ([]accountResponse, error) {
	if m.listAccountsErr != nil {
		return nil, m.listAccountsErr
	}
	return m.accounts, nil
}

// mockResendClient is a controllable implementation of resendAPI.
type mockResendClient struct {
	createDomainErr error
	verifyDomainErr error
	deleteDomainErr error

	createDomainCalls int
	verifyDomainCalls int
	deleteDomainCalls int

	domainID   string
	dnsRecords []resendDNSRecord
}

func (m *mockResendClient) createDomain(_ context.Context, _, _ string) (string, []resendDNSRecord, error) {
	m.createDomainCalls++
	if m.createDomainErr != nil {
		return "", nil, m.createDomainErr
	}
	id := m.domainID
	if id == "" {
		id = "resend-domain-id-123"
	}
	return id, m.dnsRecords, nil
}

func (m *mockResendClient) verifyDomain(_ context.Context, _ string) error {
	m.verifyDomainCalls++
	return m.verifyDomainErr
}

func (m *mockResendClient) deleteDomain(_ context.Context, _ string) error {
	m.deleteDomainCalls++
	return m.deleteDomainErr
}

// mockDNS is a controllable implementation of port.DNSProvider.
type mockDNS struct {
	upsertErr         error
	deleteByNameErr   error
	upsertCalls       []domain.DNSRecord
	deleteByNameCalls []struct{ recordType, name string }
}

func (m *mockDNS) UpsertRecord(_ context.Context, record domain.DNSRecord) (*domain.DNSRecord, error) {
	m.upsertCalls = append(m.upsertCalls, record)
	if m.upsertErr != nil {
		return nil, m.upsertErr
	}
	return &record, nil
}

func (m *mockDNS) DeleteRecordByName(_ context.Context, recordType, name string) error {
	m.deleteByNameCalls = append(m.deleteByNameCalls, struct{ recordType, name string }{recordType, name})
	return m.deleteByNameErr
}

func (m *mockDNS) CreateRecord(_ context.Context, r domain.DNSRecord) (*domain.DNSRecord, error) {
	return &r, nil
}
func (m *mockDNS) UpdateRecord(_ context.Context, _ string, r domain.DNSRecord) (*domain.DNSRecord, error) {
	return &r, nil
}
func (m *mockDNS) DeleteRecord(_ context.Context, _ string) error                   { return nil }
func (m *mockDNS) GetRecord(_ context.Context, _ string) (*domain.DNSRecord, error) { return nil, nil }
func (m *mockDNS) ListRecords(_ context.Context, _ string) ([]*domain.DNSRecord, error) {
	return nil, nil
}
func (m *mockDNS) FindRecord(_ context.Context, _, _ string) (*domain.DNSRecord, error) {
	return nil, nil
}

// --- helpers ---

func testLogger() *slog.Logger {
	return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
}

// newProvisionerWithDeps creates a Provisioner with injected dependencies for testing.
func newProvisionerWithDeps(client notifyAdminAPI, resend resendAPI, resendAPIKey string, dns port.DNSProvider, baseDomain string, logger *slog.Logger) *Provisioner {
	if baseDomain == "" {
		baseDomain = "threesix.ai"
	}
	return &Provisioner{
		client:       client,
		resend:       resend,
		resendAPIKey: resendAPIKey,
		dns:          dns,
		baseDomain:   baseDomain,
		logger:       logger,
	}
}

func newTestProvisioner(admin *mockAdminClient, resend *mockResendClient, dns *mockDNS) *Provisioner {
	var r resendAPI
	if resend != nil {
		r = resend
	}
	var d interface {
		UpsertRecord(context.Context, domain.DNSRecord) (*domain.DNSRecord, error)
		DeleteRecordByName(context.Context, string, string) error
		CreateRecord(context.Context, domain.DNSRecord) (*domain.DNSRecord, error)
		UpdateRecord(context.Context, string, domain.DNSRecord) (*domain.DNSRecord, error)
		DeleteRecord(context.Context, string) error
		GetRecord(context.Context, string) (*domain.DNSRecord, error)
		ListRecords(context.Context, string) ([]*domain.DNSRecord, error)
		FindRecord(context.Context, string, string) (*domain.DNSRecord, error)
	}
	if dns != nil {
		d = dns
	}
	return newProvisionerWithDeps(admin, r, "re_test_key", d, "test.example", testLogger())
}

// --- tests ---

func TestCreateProjectNotify_Success(t *testing.T) {
	admin := &mockAdminClient{}
	resend := &mockResendClient{
		dnsRecords: []resendDNSRecord{
			{Record: "TXT", Name: "resend._domainkey", Value: "v=DKIM1; p=..."},
			{Record: "MX", Name: "send", Value: "feedback-smtp.us-east-1.amazonses.com", Priority: 10},
		},
	}
	dns := &mockDNS{}
	p := newTestProvisioner(admin, resend, dns)

	creds, err := p.CreateProjectNotify(context.Background(), "proj-123", "happy-fox")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}

	if creds.Host != "mail.happy-fox.test.example" {
		t.Errorf("expected host mail.happy-fox.test.example, got %s", creds.Host)
	}
	if creds.From != "noreply@mail.happy-fox.test.example" {
		t.Errorf("expected from noreply@mail.happy-fox.test.example, got %s", creds.From)
	}
	if creds.APIKey != "notify_send_test_key" {
		t.Errorf("expected send key, got %s", creds.APIKey)
	}
	if creds.ResendDomainID != "resend-domain-id-123" {
		t.Errorf("expected resend domain id, got %s", creds.ResendDomainID)
	}
	if creds.ProjectID != "proj-123" {
		t.Errorf("expected project id proj-123, got %s", creds.ProjectID)
	}

	// Verify all steps executed
	if admin.createHostCalls != 1 {
		t.Errorf("expected 1 createHost call, got %d", admin.createHostCalls)
	}
	if admin.createProviderCalls != 1 {
		t.Errorf("expected 1 createProvider call, got %d", admin.createProviderCalls)
	}
	if admin.createFromAddressCalls != 1 {
		t.Errorf("expected 1 createFromAddress call, got %d", admin.createFromAddressCalls)
	}
	if admin.createAccountCalls != 1 {
		t.Errorf("expected 1 createAccount call, got %d", admin.createAccountCalls)
	}
	if admin.createSendKeyCalls != 1 {
		t.Errorf("expected 1 createSendKey call, got %d", admin.createSendKeyCalls)
	}
	if resend.createDomainCalls != 1 {
		t.Errorf("expected 1 createDomain call, got %d", resend.createDomainCalls)
	}

	// Verify 2 DNS records were upserted
	if len(dns.upsertCalls) != 2 {
		t.Errorf("expected 2 DNS upserts, got %d", len(dns.upsertCalls))
	}
}

func TestCreateProjectNotify_RollsBackOnProviderFailure(t *testing.T) {
	admin := &mockAdminClient{
		createProviderErr: errors.New("provider setup failed"),
	}
	p := newTestProvisioner(admin, &mockResendClient{}, nil)

	_, err := p.CreateProjectNotify(context.Background(), "proj-123", "happy-fox")
	if err == nil {
		t.Fatal("expected error, got nil")
	}

	if admin.deleteHostCalls != 1 {
		t.Errorf("expected host rollback, got %d deleteHost calls", admin.deleteHostCalls)
	}
}

func TestCreateProjectNotify_RollsBackOnFromAddressFailure(t *testing.T) {
	admin := &mockAdminClient{
		createFromAddressErr: errors.New("from address failed"),
	}
	p := newTestProvisioner(admin, &mockResendClient{}, nil)

	_, err := p.CreateProjectNotify(context.Background(), "proj-123", "happy-fox")
	if err == nil {
		t.Fatal("expected error, got nil")
	}

	if admin.deleteHostCalls != 1 {
		t.Errorf("expected host rollback, got %d deleteHost calls", admin.deleteHostCalls)
	}
	if admin.deleteAccountCalls != 0 {
		t.Errorf("account not yet created, should not delete account")
	}
}

func TestCreateProjectNotify_RollsBackOnAccountFailure(t *testing.T) {
	admin := &mockAdminClient{
		createAccountErr: errors.New("account creation failed"),
	}
	p := newTestProvisioner(admin, &mockResendClient{}, nil)

	_, err := p.CreateProjectNotify(context.Background(), "proj-123", "happy-fox")
	if err == nil {
		t.Fatal("expected error, got nil")
	}

	if admin.deleteHostCalls != 1 {
		t.Errorf("expected host rollback, got %d deleteHost calls", admin.deleteHostCalls)
	}
}

func TestCreateProjectNotify_RollsBackOnSendKeyFailure(t *testing.T) {
	admin := &mockAdminClient{
		createSendKeyErr: errors.New("send key creation failed"),
	}
	p := newTestProvisioner(admin, &mockResendClient{}, nil)

	_, err := p.CreateProjectNotify(context.Background(), "proj-123", "happy-fox")
	if err == nil {
		t.Fatal("expected error, got nil")
	}

	if admin.deleteAccountCalls != 1 {
		t.Errorf("expected account rollback, got %d deleteAccount calls", admin.deleteAccountCalls)
	}
	if admin.deleteHostCalls != 1 {
		t.Errorf("expected host rollback, got %d deleteHost calls", admin.deleteHostCalls)
	}
}

func TestCreateProjectNotify_ResendFailureIsNonFatal(t *testing.T) {
	admin := &mockAdminClient{}
	resend := &mockResendClient{
		createDomainErr: errors.New("resend API down"),
	}
	p := newTestProvisioner(admin, resend, nil)

	creds, err := p.CreateProjectNotify(context.Background(), "proj-123", "happy-fox")
	if err != nil {
		t.Fatalf("resend failure should be non-fatal, got error: %v", err)
	}
	if creds.ResendDomainID != "" {
		t.Errorf("expected empty resend domain id on failure, got %s", creds.ResendDomainID)
	}
}

func TestCreateProjectNotify_WithoutResend_SkipsProviderAndDomain(t *testing.T) {
	admin := &mockAdminClient{}
	p := newProvisionerWithDeps(admin, nil, "", nil, "test.example", testLogger())

	creds, err := p.CreateProjectNotify(context.Background(), "proj-123", "happy-fox")
	if err != nil {
		t.Fatalf("expected no error without resend, got %v", err)
	}

	if admin.createProviderCalls != 0 {
		t.Errorf("createProvider should not be called without ResendAPIKey, got %d calls", admin.createProviderCalls)
	}
	if creds.ResendDomainID != "" {
		t.Errorf("expected no resend domain id without ResendAPIKey, got %s", creds.ResendDomainID)
	}
}

func TestCreateProjectNotify_DNSFailureIsNonFatal(t *testing.T) {
	admin := &mockAdminClient{}
	resend := &mockResendClient{
		dnsRecords: []resendDNSRecord{{Record: "TXT", Name: "resend._domainkey", Value: "v=DKIM1"}},
	}
	dns := &mockDNS{upsertErr: errors.New("cloudflare down")}
	p := newTestProvisioner(admin, resend, dns)

	creds, err := p.CreateProjectNotify(context.Background(), "proj-123", "happy-fox")
	if err != nil {
		t.Fatalf("DNS failure should be non-fatal, got error: %v", err)
	}
	// Project still usable; DNS will need manual fix
	if creds.Host != "mail.happy-fox.test.example" {
		t.Errorf("creds should still be returned on DNS failure, got host %s", creds.Host)
	}
}

func TestDeleteProjectNotify_Success(t *testing.T) {
	admin := &mockAdminClient{
		accounts: []accountResponse{
			{ID: "acct-001", Name: "project-proj-123"},
		},
	}
	resend := &mockResendClient{}
	dns := &mockDNS{}
	p := newTestProvisioner(admin, resend, dns)

	err := p.DeleteProjectNotify(context.Background(), "proj-123", "happy-fox", "resend-domain-id-123")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}

	if admin.deleteAccountCalls != 1 {
		t.Errorf("expected 1 deleteAccount call, got %d", admin.deleteAccountCalls)
	}
	if admin.deleteHostCalls != 1 {
		t.Errorf("expected 1 deleteHost call, got %d", admin.deleteHostCalls)
	}
	if resend.deleteDomainCalls != 1 {
		t.Errorf("expected 1 deleteDomain call, got %d", resend.deleteDomainCalls)
	}
	// 3 DNS records: DKIM TXT, SPF MX, SPF TXT
	if len(dns.deleteByNameCalls) != 3 {
		t.Errorf("expected 3 DNS deleteByName calls, got %d", len(dns.deleteByNameCalls))
	}
}

func TestDeleteProjectNotify_NoResendDomainID_SkipsDomainDeletion(t *testing.T) {
	admin := &mockAdminClient{
		accounts: []accountResponse{
			{ID: "acct-001", Name: "project-proj-123"},
		},
	}
	resend := &mockResendClient{}
	p := newTestProvisioner(admin, resend, nil)

	err := p.DeleteProjectNotify(context.Background(), "proj-123", "happy-fox", "")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if resend.deleteDomainCalls != 0 {
		t.Errorf("should skip domain deletion when resendDomainID is empty")
	}
}

func TestDeleteProjectNotify_AccountNotFound_ContinuesCleanup(t *testing.T) {
	// Account doesn't exist (never provisioned or already deleted)
	admin := &mockAdminClient{accounts: []accountResponse{}}
	resend := &mockResendClient{}
	p := newTestProvisioner(admin, resend, nil)

	err := p.DeleteProjectNotify(context.Background(), "proj-123", "happy-fox", "resend-domain-id-123")
	if err != nil {
		t.Fatalf("expected no error when account not found, got %v", err)
	}
	// Should still attempt host and Resend domain deletion
	if admin.deleteHostCalls != 1 {
		t.Errorf("expected 1 deleteHost call, got %d", admin.deleteHostCalls)
	}
	if resend.deleteDomainCalls != 1 {
		t.Errorf("expected 1 deleteDomain call, got %d", resend.deleteDomainCalls)
	}
}

func TestGetProjectNotify_NotProvisioned(t *testing.T) {
	admin := &mockAdminClient{}
	p := newTestProvisioner(admin, nil, nil)

	creds, err := p.GetProjectNotify(context.Background(), "proj-123")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if creds != nil {
		t.Errorf("expected nil for unprovisioned project, got %+v", creds)
	}
}

func TestGetProjectNotify_AlreadyProvisioned(t *testing.T) {
	admin := &mockAdminClient{
		accounts: []accountResponse{
			{ID: "acct-001", Name: "project-proj-123", CreatedAt: time.Now()},
		},
	}
	p := newTestProvisioner(admin, nil, nil)

	creds, err := p.GetProjectNotify(context.Background(), "proj-123")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if creds == nil {
		t.Fatal("expected non-nil credentials for provisioned project")
	}
	if creds.AccountID != "acct-001" {
		t.Errorf("expected account id acct-001, got %s", creds.AccountID)
	}
}

func TestCreateProjectNotify_HostUsesBaseDomain(t *testing.T) {
	admin := &mockAdminClient{}
	p := newProvisionerWithDeps(admin, nil, "", nil, "staging.example.com", testLogger())

	creds, err := p.CreateProjectNotify(context.Background(), "proj-123", "some-slug")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if creds.Host != "mail.some-slug.staging.example.com" {
		t.Errorf("expected host to use baseDomain, got %s", creds.Host)
	}
}
