package main

import (
	"context"
	"errors"
	"testing"
	"time"
)

// waktuUji adalah waktu beku untuk seluruh unit test.
var waktuUji = time.Date(2026, 1, 15, 8, 0, 0, 0, time.UTC)

// genStub mengeluarkan kode dari daftar tetap secara bergilir.
type genStub struct {
	daftar []string
	i      int
}

// NewCode memenuhi interface CodeGenerator.
func (g *genStub) NewCode() (string, error) {
	code := g.daftar[g.i%len(g.daftar)]
	g.i++
	return code, nil
}

// genGagal selalu mengembalikan galat dari generator.
type genGagal struct{}

// NewCode selalu gagal — untuk menguji jalur galat generator.
func (genGagal) NewCode() (string, error) {
	return "", errors.New("entropi habis")
}

func TestValidasiURL(t *testing.T) {
	uji := []struct {
		nama string
		url  string
		ok   bool
	}{
		{"url kosong", "", false},
		{"tanpa skema", "example.com/halaman", false},
		{"skema ftp", "ftp://example.com/berkas", false},
		{"host kosong", "http:///path", false},
		{"http valid", "http://example.com/halaman?q=1", true},
		{"https valid", "https://go.dev/doc/", true},
	}
	for _, tt := range uji {
		t.Run(tt.nama, func(t *testing.T) {
			err := ValidasiURL(tt.url)
			if tt.ok && err != nil {
				t.Fatalf("ValidasiURL(%q) galat tak terduga: %v", tt.url, err)
			}
			if !tt.ok {
				var ve ValidationError
				if !errors.As(err, &ve) {
					t.Fatalf("ValidasiURL(%q) ingin ValidationError, dapat %v", tt.url, err)
				}
				if ve.Field != "url" {
					t.Errorf("field galat = %q, ingin \"url\"", ve.Field)
				}
			}
		})
	}
}

func TestShortenKonflikRetry(t *testing.T) {
	repo := NewMemoryURLRepository()
	ctx := context.Background()

	// Kode pertama dari generator sudah dipakai pemetaan lain.
	err := repo.Save(ctx, URLRecord{Code: "aaaaaaa", OriginalURL: "https://lama.example.com", CreatedAt: waktuUji})
	if err != nil {
		t.Fatalf("menyiapkan data: %v", err)
	}

	gen := &genStub{daftar: []string{"aaaaaaa", "aaaaaaa", "bbbbbbb"}}
	svc := NewShortenService(repo, gen, FixedClock{Waktu: waktuUji})

	rec, err := svc.Shorten(ctx, "https://baru.example.com/halaman")
	if err != nil {
		t.Fatalf("Shorten() galat tak terduga: %v", err)
	}
	if rec.Code != "bbbbbbb" {
		t.Errorf("kode = %q, ingin \"bbbbbbb\" setelah dua kali bentrok", rec.Code)
	}
	if !rec.CreatedAt.Equal(waktuUji) {
		t.Errorf("created_at = %v, ingin %v (waktu beku)", rec.CreatedAt, waktuUji)
	}
}

func TestShortenKodeHabis(t *testing.T) {
	repo := NewMemoryURLRepository()
	ctx := context.Background()

	_ = repo.Save(ctx, URLRecord{Code: "aaaaaaa", OriginalURL: "https://lama.example.com", CreatedAt: waktuUji})
	gen := &genStub{daftar: []string{"aaaaaaa"}} // selalu bentrok
	svc := NewShortenService(repo, gen, FixedClock{Waktu: waktuUji})

	_, err := svc.Shorten(ctx, "https://baru.example.com")
	if !errors.Is(err, ErrCodeExists) {
		t.Fatalf("Shorten() ingin ErrCodeExists setelah percobaan habis, dapat %v", err)
	}
}

func TestShortenGeneratorGagal(t *testing.T) {
	svc := NewShortenService(NewMemoryURLRepository(), genGagal{}, FixedClock{Waktu: waktuUji})
	_, err := svc.Shorten(context.Background(), "https://example.com")
	if err == nil {
		t.Fatal("Shorten() ingin galat ketika generator gagal")
	}
}

func TestRedirectMenaikkanKlik(t *testing.T) {
	repo := NewMemoryURLRepository()
	ctx := context.Background()

	gen := &genStub{daftar: []string{"b1Gk0D3"}}
	svc := NewShortenService(repo, gen, FixedClock{Waktu: waktuUji})

	rec, err := svc.Shorten(ctx, "https://example.com/tutorial")
	if err != nil {
		t.Fatalf("Shorten(): %v", err)
	}

	for i := 1; i <= 2; i++ {
		asli, err := svc.Redirect(ctx, rec.Code)
		if err != nil {
			t.Fatalf("Redirect() ke-%d: %v", i, err)
		}
		if asli != "https://example.com/tutorial" {
			t.Errorf("Redirect() ke-%d = %q, ingin URL asli", i, asli)
		}
	}

	stat, err := svc.Stats(ctx, rec.Code)
	if err != nil {
		t.Fatalf("Stats(): %v", err)
	}
	if stat.Clicks != 2 {
		t.Errorf("klik = %d, ingin 2 setelah dua redirect", stat.Clicks)
	}
}

func TestRedirectKodeTakDikenal(t *testing.T) {
	svc := NewShortenService(NewMemoryURLRepository(), &genStub{daftar: []string{"b1Gk0D3"}}, FixedClock{Waktu: waktuUji})
	_, err := svc.Redirect(context.Background(), "takDikn")
	if !errors.Is(err, ErrNotFound) {
		t.Fatalf("Redirect() ingin ErrNotFound, dapat %v", err)
	}
}
