package main

import (
	"errors"
	"fmt"
)

// ErrEmailTerdaftar adalah error domain: pendaftaran ganda.
var ErrEmailTerdaftar = errors.New("email sudah terdaftar")

// UserRepo adalah kontrak penyimpanan pengguna.
type UserRepo interface {
	Save(nama, email string) error
	Exists(email string) bool
}

// Notifier adalah kontrak pengiriman notifikasi.
type Notifier interface {
	Send(email, pesan string) error
}

// LayananRegistrasi adalah komponen inti: hanya mengenal kontrak.
// Seluruh dependensi masuk lewat constructor NewLayananRegistrasi.
type LayananRegistrasi struct {
	repo     UserRepo
	notifier Notifier
}

func NewLayananRegistrasi(repo UserRepo, notifier Notifier) *LayananRegistrasi {
	return &LayananRegistrasi{repo: repo, notifier: notifier}
}

func (l *LayananRegistrasi) Daftar(nama, email string) error {
	if l.repo.Exists(email) {
		return fmt.Errorf("daftar %q: %w", email, ErrEmailTerdaftar)
	}
	if err := l.repo.Save(nama, email); err != nil {
		return err
	}
	return l.notifier.Send(email, "Halo "+nama+", selamat bergabung!")
}

// MemoryUserRepo adalah implementasi sungguhan.
// Di produksi isinya database; di sini cukup map.
type MemoryUserRepo struct {
	pengguna map[string]string // email -> nama
}

func NewMemoryUserRepo() *MemoryUserRepo {
	return &MemoryUserRepo{pengguna: map[string]string{}}
}

func (r *MemoryUserRepo) Save(nama, email string) error {
	r.pengguna[email] = nama
	return nil
}

func (r *MemoryUserRepo) Exists(email string) bool {
	_, ok := r.pengguna[email]
	return ok
}

// SMTPNotifier adalah implementasi notifikasi sungguhan.
type SMTPNotifier struct {
	Host string
}

func (n *SMTPNotifier) Send(email, pesan string) error {
	fmt.Printf("[smtp %s] kirim ke %s: %s\n", n.Host, email, pesan)
	return nil
}

// FakeNotifier menangkap pesan tanpa mengirim apa pun.
// Objek semacam ini dipakai sebagai pengganti saat pengujian.
type FakeNotifier struct {
	Terkirim []string
}

func (f *FakeNotifier) Send(email, pesan string) error {
	f.Terkirim = append(f.Terkirim, email+" <- "+pesan)
	return nil
}

func main() {
	// Composition root: satu-satunya tempat yang tahu cara
	// merakit seluruh komponen menjadi aplikasi utuh.
	repo := NewMemoryUserRepo()
	notifier := &SMTPNotifier{Host: "smtp.example.com"}
	app := NewLayananRegistrasi(repo, notifier)

	for _, p := range [][2]string{
		{"Budi", "budi@example.com"},
		{"Siti", "siti@example.com"},
		{"Budi Kedua", "budi@example.com"},
	} {
		if err := app.Daftar(p[0], p[1]); err != nil {
			fmt.Println("gagal :", err)
			fmt.Println("errors.Is(ErrEmailTerdaftar) =", errors.Is(err, ErrEmailTerdaftar))
		}
	}

	// Skenario uji: SMTP diganti fake, kode layanan tidak berubah.
	fake := &FakeNotifier{}
	appUji := NewLayananRegistrasi(NewMemoryUserRepo(), fake)
	if err := appUji.Daftar("Siti", "siti@example.com"); err != nil {
		fmt.Println("gagal :", err)
		return
	}
	fmt.Printf("fake menangkap %d pesan:\n", len(fake.Terkirim))
	for _, m := range fake.Terkirim {
		fmt.Println(" -", m)
	}
}
