//go:build integration

package services_test

// Test de integración del ciclo completo de notificaciones:
//   1. Envío de correo de bienvenida al crear contrato
//   2. Marca del contrato como pagado
//   3. Envío automático de confirmación de pago
//   4. Idempotencia: no reenviar si ya se envió hoy
//
// Requisitos para ejecutar:
//   export TEST_DATABASE_URL="host=localhost user=postgres password=xxx dbname=soft_usite_test port=5432 sslmode=disable"
//   go test -v -tags integration ./pkg/services/ -run TestCicloNotificaciones

import (
	"bufio"
	"fmt"
	"net"
	"os"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/sujit-baniya/fiber-boilerplate/app"
	"github.com/sujit-baniya/fiber-boilerplate/config"
	"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
	"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
	mail "github.com/xhit/go-simple-mail/v2"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
	gormlogger "gorm.io/gorm/logger"
)

// ─── Servidor SMTP falso ──────────────────────────────────────────────────────

// fakeSMTP implementa el mínimo del protocolo SMTP para aceptar envíos de
// go-simple-mail sin cifrado. No valida credenciales.
type fakeSMTP struct {
	listener net.Listener
	mu       sync.Mutex
	emails   []capturedEmail
}

type capturedEmail struct {
	To   string
	Body string
}

func newFakeSMTP(t *testing.T) *fakeSMTP {
	t.Helper()
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("fakeSMTP listen: %v", err)
	}
	s := &fakeSMTP{listener: ln}
	go s.serve()
	return s
}

func (s *fakeSMTP) Port() int {
	return s.listener.Addr().(*net.TCPAddr).Port
}

func (s *fakeSMTP) Close() {
	s.listener.Close()
}

func (s *fakeSMTP) Emails() []capturedEmail {
	s.mu.Lock()
	defer s.mu.Unlock()
	cp := make([]capturedEmail, len(s.emails))
	copy(cp, s.emails)
	return cp
}

func (s *fakeSMTP) Count() int {
	s.mu.Lock()
	defer s.mu.Unlock()
	return len(s.emails)
}

func (s *fakeSMTP) serve() {
	for {
		conn, err := s.listener.Accept()
		if err != nil {
			return // listener cerrado
		}
		go s.handleConn(conn)
	}
}

func (s *fakeSMTP) handleConn(conn net.Conn) {
	defer conn.Close()
	w := bufio.NewWriter(conn)
	r := bufio.NewReader(conn)

	send := func(line string) {
		fmt.Fprintf(w, "%s\r\n", line)
		w.Flush()
	}

	send("220 fakesmtp ESMTP ready")

	var currentTo string
	var inData bool
	var body strings.Builder

	for {
		line, err := r.ReadString('\n')
		if err != nil {
			return
		}
		line = strings.TrimRight(line, "\r\n")

		if inData {
			if line == "." {
				s.mu.Lock()
				s.emails = append(s.emails, capturedEmail{To: currentTo, Body: body.String()})
				s.mu.Unlock()
				body.Reset()
				currentTo = ""
				inData = false
				send("250 Message queued")
			} else {
				// El protocolo SMTP usa "byte stuffing" con punto inicial: strip leading dot
				if strings.HasPrefix(line, "..") {
					line = line[1:]
				}
				body.WriteString(line + "\n")
			}
			continue
		}

		upper := strings.ToUpper(line)
		switch {
		case strings.HasPrefix(upper, "EHLO"), strings.HasPrefix(upper, "HELO"):
			send("250-fakesmtp\r\n250 AUTH PLAIN LOGIN")
		case strings.HasPrefix(upper, "AUTH"):
			send("235 Authentication successful")
		case strings.HasPrefix(upper, "MAIL FROM"):
			send("250 OK")
		case strings.HasPrefix(upper, "RCPT TO"):
			start := strings.Index(line, "<")
			end := strings.LastIndex(line, ">")
			if start >= 0 && end > start {
				currentTo = line[start+1 : end]
			}
			send("250 OK")
		case upper == "DATA":
			inData = true
			send("354 End data with <CR><LF>.<CR><LF>")
		case strings.HasPrefix(upper, "RSET"):
			currentTo = ""
			body.Reset()
			inData = false
			send("250 OK")
		case strings.HasPrefix(upper, "QUIT"):
			send("221 Bye")
			return
		default:
			send("500 Unknown command")
		}
	}
}

// ─── Helpers de test ──────────────────────────────────────────────────────────

func setupTestDB(t *testing.T) *gorm.DB {
	t.Helper()
	dsn := os.Getenv("TEST_DATABASE_URL")
	if dsn == "" {
		t.Skip("TEST_DATABASE_URL no configurado — omitiendo test de integración.\n" +
			"Ejemplo: export TEST_DATABASE_URL=\"host=localhost user=postgres password=xxx dbname=soft_usite_test port=5432 sslmode=disable\"")
	}
	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
		Logger: gormlogger.Default.LogMode(gormlogger.Silent),
	})
	if err != nil {
		t.Fatalf("no se pudo conectar a la DB de test: %v", err)
	}
	return db
}

// bootstrapApp inicializa app.Http con la DB y el SMTP falso dados.
func bootstrapApp(db *gorm.DB, smtp *fakeSMTP) {
	if app.Http == nil {
		app.Http = &config.AppConfig{}
	}

	// DB
	app.Http.Database.DB = db

	// Mail — configurar SMTPServer apuntando al fake server
	smtpServer := mail.NewSMTPClient()
	smtpServer.Host = "127.0.0.1"
	smtpServer.Port = smtp.Port()
	smtpServer.Encryption = mail.EncryptionNone
	smtpServer.ConnectTimeout = 5 * time.Second
	smtpServer.SendTimeout = 10 * time.Second
	// Sin Username para evitar AUTH obligatorio

	app.Http.Mail.SMTPServer = smtpServer
	app.Http.Mail.Host = "127.0.0.1"
	app.Http.Mail.Port = smtp.Port()
	app.Http.Mail.Encryption = "none"
	app.Http.Mail.FromAddress = "test@sistema.local"
	app.Http.Mail.FromName = "Test Sistema"
}

// waitFor espera hasta que la condición sea true o timeout.
func waitFor(t *testing.T, label string, timeout time.Duration, cond func() bool) {
	t.Helper()
	deadline := time.Now().Add(timeout)
	for time.Now().Before(deadline) {
		if cond() {
			return
		}
		time.Sleep(50 * time.Millisecond)
	}
	t.Errorf("timeout esperando: %s", label)
}

// ─── Test principal ───────────────────────────────────────────────────────────

func TestCicloNotificaciones(t *testing.T) {
	db := setupTestDB(t)
	smtp := newFakeSMTP(t)
	defer smtp.Close()
	bootstrapApp(db, smtp)

	// ── Sufijo único para no colisionar con datos reales ──────────────────────
	suffix := fmt.Sprintf("_test_%d", time.Now().UnixNano())
	emailTest := fmt.Sprintf("ciclo%s@test.local", suffix)

	// ── Crear datos de prueba ─────────────────────────────────────────────────

	plantBienvenida := models.PlantillaCorreo{
		Nombre:     "TEST Bienvenida" + suffix,
		Asunto:     "Bienvenido al sistema",
		CuerpoHTML: "<p>Hola {{.ClienteNombre}}, bienvenido a {{.ClienteEmpresa}}.</p>",
		Tipo:       "bienvenida",
	}
	if err := db.Create(&plantBienvenida).Error; err != nil {
		t.Fatalf("crear plantilla bienvenida: %v", err)
	}

	plantPago := models.PlantillaCorreo{
		Nombre:     "TEST Pago Confirmado" + suffix,
		Asunto:     "Tu pago fue recibido",
		CuerpoHTML: "<p>Hola {{.ClienteNombre}}, recibimos tu pago de ${{.Total}}.</p>",
		Tipo:       "pago",
	}
	if err := db.Create(&plantPago).Error; err != nil {
		t.Fatalf("crear plantilla pago: %v", err)
	}

	reglaBienvenida := models.NotificacionRegla{
		Nombre:         "TEST Regla Bienvenida" + suffix,
		TipoEvento:     "bienvenida",
		PlantillaID:    plantBienvenida.ID,
		Activo:         true,
		AplicaA:        "todos",
		PasarelaEnlace: "ninguna", // evita llamadas a Bold/dLocal
	}
	if err := db.Create(&reglaBienvenida).Error; err != nil {
		t.Fatalf("crear regla bienvenida: %v", err)
	}

	reglaPago := models.NotificacionRegla{
		Nombre:         "TEST Regla Pago" + suffix,
		TipoEvento:     "pago_recibido",
		PlantillaID:    plantPago.ID,
		Activo:         true,
		AplicaA:        "todos",
		PasarelaEnlace: "ninguna",
	}
	if err := db.Create(&reglaPago).Error; err != nil {
		t.Fatalf("crear regla pago: %v", err)
	}

	cliente := models.Cliente{
		Nombre:  "Cliente Test" + suffix,
		Email:   emailTest,
		Empresa: "Empresa Test",
		Activo:  true,
	}
	if err := db.Create(&cliente).Error; err != nil {
		t.Fatalf("crear cliente: %v", err)
	}

	now := time.Now()
	contrato := models.Contrato{
		ClienteID:        cliente.ID,
		Cliente:          cliente,
		FechaInicio:      now,
		FechaVencimiento: now.AddDate(1, 0, 0),
		PrecioAcordado:   150.0,
		Estado:           "activo",
	}
	if err := db.Create(&contrato).Error; err != nil {
		t.Fatalf("crear contrato: %v", err)
	}
	contrato.Cliente = cliente // relación en memoria para el servicio

	// ── Cleanup al finalizar ─────────────────────────────────────────────────
	t.Cleanup(func() {
		// notificaciones_log → contratos → clientes → reglas → plantillas
		db.Unscoped().Where("cliente_id = ?", cliente.ID).Delete(&models.NotificacionLog{})
		db.Unscoped().Delete(&contrato)
		db.Unscoped().Delete(&cliente)
		db.Unscoped().Delete(&reglaBienvenida)
		db.Unscoped().Delete(&reglaPago)
		db.Unscoped().Delete(&plantBienvenida)
		db.Unscoped().Delete(&plantPago)
	})

	// ══════════════════════════════════════════════════════════════════════════
	// PASO 1 — Correo de bienvenida
	// ══════════════════════════════════════════════════════════════════════════
	t.Run("paso1_bienvenida", func(t *testing.T) {
		countBefore := smtp.Count()

		err := services.EnviarNotificacionGrupo(
			&reglaBienvenida,
			&cliente,
			[]models.Contrato{contrato},
			"",
		)
		if err != nil {
			t.Fatalf("EnviarNotificacionGrupo (bienvenida): %v", err)
		}

		// Esperar entrega al fake SMTP
		waitFor(t, "email de bienvenida recibido", 3*time.Second, func() bool {
			return smtp.Count() > countBefore
		})

		emails := smtp.Emails()
		ultimo := emails[len(emails)-1]
		if ultimo.To != emailTest {
			t.Errorf("destinatario: esperado %q, recibido %q", emailTest, ultimo.To)
		}
		if !strings.Contains(ultimo.Body, "Bienvenido") || !strings.Contains(ultimo.Body, cliente.Nombre) {
			t.Errorf("cuerpo del email no contiene el contenido esperado.\nBody:\n%s", ultimo.Body)
		}

		// Verificar log en DB
		var logCount int64
		db.Model(&models.NotificacionLog{}).
			Where("cliente_id = ? AND regla_id = ? AND estado = 'enviado'", cliente.ID, reglaBienvenida.ID).
			Count(&logCount)
		if logCount == 0 {
			t.Error("no se encontró log de bienvenida con estado='enviado' en la DB")
		}
		t.Logf("✓ Email de bienvenida enviado a %s — logs en DB: %d", emailTest, logCount)
	})

	// ══════════════════════════════════════════════════════════════════════════
	// PASO 2 — Recibir pago → marcar contrato → enviar confirmación
	// ══════════════════════════════════════════════════════════════════════════
	t.Run("paso2_pago_confirmado", func(t *testing.T) {
		// 2a. Simular llegada del webhook: marcar contrato como pagado
		if _, err := models.MarcarContratoPagado(contrato.ID); err != nil {
			t.Fatalf("MarcarContratoPagado: %v", err)
		}

		// 2b. Verificar campo en DB
		var c models.Contrato
		db.Select("pago_confirmado, fecha_pago").First(&c, contrato.ID)
		if !c.PagoConfirmado {
			t.Error("contrato.pago_confirmado debería ser true tras MarcarContratoPagado")
		}
		if c.FechaPago == nil {
			t.Error("contrato.fecha_pago debería tener valor")
		}
		t.Logf("✓ Contrato %d marcado como pagado en %s", contrato.ID, c.FechaPago.Format("02/01/2006 15:04"))

		// 2c. Disparar correo de confirmación (igual que hace el webhook)
		countBefore := smtp.Count()
		services.EnviarCorreoConfirmacionPago(contrato.ID)

		waitFor(t, "email de confirmación de pago recibido", 3*time.Second, func() bool {
			return smtp.Count() > countBefore
		})

		emails := smtp.Emails()
		ultimo := emails[len(emails)-1]
		if ultimo.To != emailTest {
			t.Errorf("destinatario pago: esperado %q, recibido %q", emailTest, ultimo.To)
		}
		if !strings.Contains(ultimo.Body, "pago") && !strings.Contains(strings.ToLower(ultimo.Body), "pago") {
			t.Logf("advertencia: cuerpo no contiene 'pago' — puede ser OK según la plantilla.\nBody:\n%s", ultimo.Body)
		}

		// Verificar log en DB
		var logCount int64
		db.Model(&models.NotificacionLog{}).
			Where("cliente_id = ? AND regla_id = ? AND estado = 'enviado'", cliente.ID, reglaPago.ID).
			Count(&logCount)
		if logCount == 0 {
			t.Error("no se encontró log de confirmación de pago con estado='enviado' en la DB")
		}
		t.Logf("✓ Email de confirmación de pago enviado a %s — logs en DB: %d", emailTest, logCount)
	})

	// ══════════════════════════════════════════════════════════════════════════
	// PASO 3 — Idempotencia: un segundo intento NO debe reenviar
	// ══════════════════════════════════════════════════════════════════════════
	t.Run("paso3_idempotencia", func(t *testing.T) {
		countBefore := smtp.Count()

		// Llamar de nuevo como si llegara un webhook duplicado
		services.EnviarCorreoConfirmacionPago(contrato.ID)
		time.Sleep(300 * time.Millisecond) // dar tiempo a que lo intente si hubiera bug

		if smtp.Count() > countBefore {
			t.Errorf("YaEnviadoHoy debería bloquear el reenvío, pero se enviaron %d email(s) extra",
				smtp.Count()-countBefore)
		} else {
			t.Logf("✓ Idempotencia OK — no se reenvió el correo de confirmación")
		}
	})

	// ══════════════════════════════════════════════════════════════════════════
	// RESUMEN
	// ══════════════════════════════════════════════════════════════════════════
	t.Logf("\nResumen SMTP fake — total emails recibidos: %d", smtp.Count())
	for i, e := range smtp.Emails() {
		preview := e.Body
		if len(preview) > 80 {
			preview = preview[:80] + "..."
		}
		t.Logf("  [%d] To:%s — %s", i+1, e.To, preview)
	}
}
