package main

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"sort"
	"strings"
	"sync/atomic"
	"testing"
	"time"
)

// La allowlist del proxy debe ser SOLO de lectura: cualquier escritura o búsqueda
// de clientes se hace server-side en /api/book, nunca por el proxy.
func TestEaRouteAllowed(t *testing.T) {
	cases := []struct {
		method string
		path   string
		want   bool
	}{
		// Lecturas permitidas (las que usa el frontend)
		{"GET", "/ea-api/v1/categories", true},
		{"GET", "/ea-api/v1/service_categories", true},
		{"GET", "/ea-api/v1/services", true},
		{"GET", "/ea-api/v1/availabilities", true},
		{"GET", "/ea-api/v1/providers/1", true},
		{"GET", "/ea-api/v1/settings/company_working_plan", true},

		// Bloqueadas: clientes y citas ya NO pasan por el proxy
		{"GET", "/ea-api/v1/customers", false},
		{"POST", "/ea-api/v1/customers", false},
		{"POST", "/ea-api/v1/appointments", false},
		{"GET", "/ea-api/v1/appointments", false},
		{"DELETE", "/ea-api/v1/appointments/3", false},
		{"PUT", "/ea-api/v1/customers/5", false},
		{"GET", "/ea-api/v1/providers", false},     // listar todos
		{"GET", "/ea-api/v1/providers/abc", false}, // id no numérico
		{"GET", "/ea-api/v1/settings", false},
		{"GET", "/ea-api/v1/settings/email_footer", false}, // otros settings no
		{"POST", "/ea-api/v1/settings/company_working_plan", false},
		{"POST", "/ea-api/v1/services", false},
	}
	for _, c := range cases {
		if got := eaRouteAllowed(c.method, c.path); got != c.want {
			t.Errorf("eaRouteAllowed(%q, %q) = %v, want %v", c.method, c.path, got, c.want)
		}
	}
}

func TestFlexIntUnmarshal(t *testing.T) {
	var s struct {
		A flexInt `json:"a"`
		B flexInt `json:"b"`
		C flexInt `json:"c"`
		D flexInt `json:"d"`
	}
	if err := json.Unmarshal([]byte(`{"a":30,"b":"45","c":"60.0","d":null}`), &s); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if s.A != 30 || s.B != 45 || s.C != 60 || s.D != 0 {
		t.Errorf("got %d,%d,%d,%d want 30,45,60,0", s.A, s.B, s.C, s.D)
	}
}

// TestHandleBook comprueba que una reserva se orquesta ENTERAMENTE contra la API
// de EasyAppointments: lee servicios, crea el cliente y crea la cita, calculando
// duración total y servicio principal en el servidor.
func TestHandleBook(t *testing.T) {
	var gotAppt eaAppointment
	var gotCust eaCustomer
	var hits []string

	mux := http.NewServeMux()
	mux.HandleFunc("/index.php/api/v1/services", func(w http.ResponseWriter, r *http.Request) {
		hits = append(hits, "GET services")
		json.NewEncoder(w).Encode([]eaService{
			{ID: 1, Name: "Corte", Duration: 30},
			{ID: 2, Name: "Tinte", Duration: 45},
		})
	})
	mux.HandleFunc("/index.php/api/v1/customers", func(w http.ResponseWriter, r *http.Request) {
		if r.Method == http.MethodGet {
			hits = append(hits, "GET customers")
			json.NewEncoder(w).Encode([]eaCustomer{}) // no existe → se crea
			return
		}
		hits = append(hits, "POST customers")
		json.NewDecoder(r.Body).Decode(&gotCust)
		json.NewEncoder(w).Encode(eaCustomer{ID: 99})
	})
	mux.HandleFunc("/index.php/api/v1/appointments", func(w http.ResponseWriter, r *http.Request) {
		hits = append(hits, "POST appointments")
		json.NewDecoder(r.Body).Decode(&gotAppt)
		w.WriteHeader(http.StatusCreated)
		w.Write([]byte(`{"id":5}`))
	})
	mux.HandleFunc("/index.php/api/v1/providers", func(w http.ResponseWriter, r *http.Request) {
		hits = append(hits, "GET providers")
		json.NewEncoder(w).Encode([]eaProvider{{ID: 7}})
	})
	srv := httptest.NewServer(mux)
	defer srv.Close()

	ea := &eaClient{base: srv.URL, http: srv.Client()}

	body := `{"nombre":"Ana","apellidos":"López Gil","email":"ana@x.com","telefono":"600","mensaje":"hola",
	          "serviceIds":[1,2],"date":"2026-07-01","time":"10:00"}`
	req := httptest.NewRequest(http.MethodPost, "/api/book", strings.NewReader(body))
	rec := httptest.NewRecorder()
	handleBook(ea)(rec, req)

	if rec.Code != http.StatusCreated {
		t.Fatalf("status = %d, want 201; body=%s", rec.Code, rec.Body.String())
	}
	// Servicio principal = el de mayor duración (Tinte, id 2).
	if gotAppt.ServiceID != 2 {
		t.Errorf("serviceId = %d, want 2 (mayor duración)", gotAppt.ServiceID)
	}
	// Fin = inicio + 30 + 45 = 75 min.
	if gotAppt.Start != "2026-07-01 10:00:00" || gotAppt.End != "2026-07-01 11:15:00" {
		t.Errorf("start/end = %q/%q, want 10:00:00/11:15:00", gotAppt.Start, gotAppt.End)
	}
	// El provider lo resuelve el servidor contra EA, no el navegador.
	if gotAppt.CustomerID != 99 || gotAppt.ProviderID != 7 || gotAppt.Status != "booked" {
		t.Errorf("appt = %+v", gotAppt)
	}
	// Nombre y apellidos viajan por separado hasta EA, sin adivinar cuál es cuál.
	if gotCust.FirstName != "Ana" || gotCust.LastName != "López Gil" {
		t.Errorf("cliente = %q %q, want \"Ana\" \"López Gil\"", gotCust.FirstName, gotCust.LastName)
	}
	if !strings.Contains(gotAppt.Notes, "Duración total: 75 min") ||
		!strings.Contains(gotAppt.Notes, "Servicios: Corte + Tinte") {
		t.Errorf("notes = %q", gotAppt.Notes)
	}
	// Toda la gestión pasó por EA y nada más.
	sort.Strings(hits)
	want := []string{"GET customers", "GET providers", "GET services", "POST appointments", "POST customers"}
	if strings.Join(hits, ",") != strings.Join(want, ",") {
		t.Errorf("llamadas a EA = %v, want %v", hits, want)
	}
}

// El teléfono identifica al cliente: si ya existe en EA se reutiliza (no se
// duplica) y se le pueden crear varias citas. El email ya no es obligatorio.
func TestHandleBookReusesCustomerByPhone(t *testing.T) {
	customersCreated := 0
	apptsCreated := 0
	var lastAppt eaAppointment

	mux := http.NewServeMux()
	mux.HandleFunc("/index.php/api/v1/services", func(w http.ResponseWriter, r *http.Request) {
		json.NewEncoder(w).Encode([]eaService{{ID: 1, Name: "Corte", Duration: 30}})
	})
	mux.HandleFunc("/index.php/api/v1/providers", func(w http.ResponseWriter, r *http.Request) {
		json.NewEncoder(w).Encode([]eaProvider{{ID: 7}})
	})
	mux.HandleFunc("/index.php/api/v1/customers", func(w http.ResponseWriter, r *http.Request) {
		if r.Method == http.MethodGet {
			if q := r.URL.Query().Get("q"); q != "690918815" {
				t.Errorf("q = %q, want los últimos 9 dígitos del teléfono", q)
			}
			// En EA está guardado con prefijo y espacios: debe reconocerse igual.
			json.NewEncoder(w).Encode([]eaCustomer{{ID: 42, Phone: "+34 690 91 88 15"}})
			return
		}
		customersCreated++
		json.NewEncoder(w).Encode(eaCustomer{ID: 99})
	})
	mux.HandleFunc("/index.php/api/v1/appointments", func(w http.ResponseWriter, r *http.Request) {
		apptsCreated++
		json.NewDecoder(r.Body).Decode(&lastAppt)
		w.WriteHeader(http.StatusCreated)
	})
	srv := httptest.NewServer(mux)
	defer srv.Close()
	ea := &eaClient{base: srv.URL, http: srv.Client()}

	// Dos reservas sin email y con el teléfono escrito distinto cada vez.
	for _, tel := range []string{"690 918 815", "+34690918815"} {
		body := `{"nombre":"Ana","apellidos":"López","telefono":"` + tel + `","serviceIds":[1],"date":"2026-07-01","time":"10:00"}`
		rec := httptest.NewRecorder()
		handleBook(ea)(rec, httptest.NewRequest(http.MethodPost, "/api/book", strings.NewReader(body)))
		if rec.Code != http.StatusCreated {
			t.Fatalf("status = %d, want 201; body=%s", rec.Code, rec.Body.String())
		}
	}

	if customersCreated != 0 {
		t.Errorf("se crearon %d clientes nuevos; ese teléfono ya existía en EA", customersCreated)
	}
	if apptsCreated != 2 {
		t.Errorf("citas creadas = %d, want 2 (varias citas para el mismo cliente)", apptsCreated)
	}
	if lastAppt.CustomerID != 42 {
		t.Errorf("customerId = %d, want 42 (el cliente existente)", lastAppt.CustomerID)
	}
}

func TestSamePhone(t *testing.T) {
	cases := []struct {
		a, b string
		want bool
	}{
		{"690918815", "690 91 88 15", true},
		{"+34 690918815", "690918815", true},
		{"0034690918815", "690.91.88.15", true},
		{"690918815", "690918816", false},
		{"600", "600", true},
		{"600", "222600", false}, // sufijos cortos no identifican
		{"", "690918815", false},
	}
	for _, c := range cases {
		if got := samePhone(c.a, c.b); got != c.want {
			t.Errorf("samePhone(%q, %q) = %v, want %v", c.a, c.b, got, c.want)
		}
	}
}

// Servicio inválido (no existe en EA) ⇒ 400 y NO se crea ninguna cita.
func TestHandleBookRejectsUnknownService(t *testing.T) {
	created := false
	mux := http.NewServeMux()
	mux.HandleFunc("/index.php/api/v1/services", func(w http.ResponseWriter, r *http.Request) {
		json.NewEncoder(w).Encode([]eaService{{ID: 1, Name: "Corte", Duration: 30}})
	})
	mux.HandleFunc("/index.php/api/v1/appointments", func(w http.ResponseWriter, r *http.Request) {
		created = true
	})
	srv := httptest.NewServer(mux)
	defer srv.Close()
	ea := &eaClient{base: srv.URL, http: srv.Client()}

	body := `{"nombre":"Ana","apellidos":"López","email":"a@x.com","telefono":"600","serviceIds":[999],"date":"2026-07-01","time":"10:00"}`
	rec := httptest.NewRecorder()
	handleBook(ea)(rec, httptest.NewRequest(http.MethodPost, "/api/book", strings.NewReader(body)))

	if rec.Code != http.StatusBadRequest {
		t.Errorf("status = %d, want 400", rec.Code)
	}
	if created {
		t.Error("se creó una cita con un servicio inexistente")
	}
}

// El id del provider se resuelve contra EA (autodescubierto) o vía
// EA_PROVIDER_ID, nunca hardcodeado: es distinto en cada entorno.
func TestProviderID(t *testing.T) {
	calls := 0
	mux := http.NewServeMux()
	mux.HandleFunc("/index.php/api/v1/providers", func(w http.ResponseWriter, r *http.Request) {
		calls++
		json.NewEncoder(w).Encode([]eaProvider{{ID: 5}, {ID: 9}})
	})
	srv := httptest.NewServer(mux)
	defer srv.Close()
	ea := &eaClient{base: srv.URL, http: srv.Client()}

	t.Run("autodescubre el primero de EA", func(t *testing.T) {
		id, err := ea.providerID()
		if err != nil || id != 5 {
			t.Errorf("providerID() = %d, %v; want 5, nil", id, err)
		}
	})

	t.Run("EA_PROVIDER_ID tiene prioridad y no llama a EA", func(t *testing.T) {
		t.Setenv("EA_PROVIDER_ID", "9")
		before := calls
		id, err := ea.providerID()
		if err != nil || id != 9 {
			t.Errorf("providerID() = %d, %v; want 9, nil", id, err)
		}
		if calls != before {
			t.Error("con EA_PROVIDER_ID definido no debería consultar EA")
		}
	})

	t.Run("EA_PROVIDER_ID inválido es error", func(t *testing.T) {
		t.Setenv("EA_PROVIDER_ID", "mayra")
		if _, err := ea.providerID(); err == nil {
			t.Error("EA_PROVIDER_ID no numérico debería dar error")
		}
	})

	t.Run("sin providers en EA es error", func(t *testing.T) {
		empty := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.Write([]byte(`[]`))
		}))
		defer empty.Close()
		ea := &eaClient{base: empty.URL, http: empty.Client()}
		if _, err := ea.providerID(); err == nil {
			t.Error("sin providers debería dar error")
		}
	})

	t.Run("/api/config lo expone al frontend", func(t *testing.T) {
		rec := httptest.NewRecorder()
		handleConfig(ea)(rec, httptest.NewRequest(http.MethodGet, "/api/config", nil))
		if rec.Code != http.StatusOK {
			t.Fatalf("status = %d, want 200", rec.Code)
		}
		var got struct {
			ProviderID int `json:"providerId"`
		}
		if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil || got.ProviderID != 5 {
			t.Errorf("body = %s (err %v), want providerId 5", rec.Body.String(), err)
		}
	})
}

// /api/month-availability agrega las availabilities de EA día a día: devuelve
// los días sin ningún hueco para que el calendario los deshabilite, y cachea
// el resultado (una sola pasada de llamadas a EA por mes+servicio).
func TestHandleMonthAvailability(t *testing.T) {
	// Mes siguiente al actual: así ningún día es pasado y se consultan todos.
	now := time.Now()
	first := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC).AddDate(0, 1, 0)
	month := first.Format("2006-01")
	daysInMonth := first.AddDate(0, 1, -1).Day()
	full1, full2 := month+"-15", month+"-16"

	var eaCalls atomic.Int64
	mux := http.NewServeMux()
	mux.HandleFunc("/index.php/api/v1/providers", func(w http.ResponseWriter, r *http.Request) {
		json.NewEncoder(w).Encode([]eaProvider{{ID: 7}})
	})
	mux.HandleFunc("/index.php/api/v1/availabilities", func(w http.ResponseWriter, r *http.Request) {
		eaCalls.Add(1)
		if r.URL.Query().Get("serviceId") != "3" || r.URL.Query().Get("providerId") != "7" {
			t.Errorf("query inesperada: %s", r.URL.RawQuery)
		}
		if date := r.URL.Query().Get("date"); date == full1 || date == full2 {
			w.Write([]byte(`[]`)) // día completo: sin ningún hueco
			return
		}
		w.Write([]byte(`["10:00","10:30"]`))
	})
	srv := httptest.NewServer(mux)
	defer srv.Close()
	handler := handleMonthAvailability(&eaClient{base: srv.URL, http: srv.Client()})

	get := func() *httptest.ResponseRecorder {
		rec := httptest.NewRecorder()
		handler(rec, httptest.NewRequest(http.MethodGet, "/api/month-availability?month="+month+"&serviceId=3", nil))
		return rec
	}

	rec := get()
	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
	}
	var got struct {
		UnavailableDays []string `json:"unavailableDays"`
	}
	if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
		t.Fatalf("json: %v", err)
	}
	if strings.Join(got.UnavailableDays, ",") != full1+","+full2 {
		t.Errorf("unavailableDays = %v, want [%s %s]", got.UnavailableDays, full1, full2)
	}
	if n := eaCalls.Load(); n != int64(daysInMonth) {
		t.Errorf("llamadas a EA = %d, want %d (una por día del mes)", n, daysInMonth)
	}

	// Segunda petición: servida desde la caché, sin más llamadas a EA.
	if rec := get(); rec.Code != http.StatusOK {
		t.Fatalf("status con caché = %d, want 200", rec.Code)
	}
	if n := eaCalls.Load(); n != int64(daysInMonth) {
		t.Errorf("la caché no se usó: %d llamadas a EA", n)
	}

	// Parámetros inválidos ⇒ 400 (meses pasados, sin servicio, formato malo).
	for _, qs := range []string{
		"month=2020-01&serviceId=3",
		"month=" + month,
		"month=13-2026&serviceId=3",
	} {
		rec := httptest.NewRecorder()
		handler(rec, httptest.NewRequest(http.MethodGet, "/api/month-availability?"+qs, nil))
		if rec.Code != http.StatusBadRequest {
			t.Errorf("status = %d, want 400 (%s)", rec.Code, qs)
		}
	}
}

// Sin apellidos ⇒ 400 sin llegar a llamar a EA.
func TestHandleBookRequiresApellidos(t *testing.T) {
	called := false
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { called = true })
	srv := httptest.NewServer(mux)
	defer srv.Close()
	ea := &eaClient{base: srv.URL, http: srv.Client()}

	body := `{"nombre":"Ana","email":"a@x.com","telefono":"600","serviceIds":[1],"date":"2026-07-01","time":"10:00"}`
	rec := httptest.NewRecorder()
	handleBook(ea)(rec, httptest.NewRequest(http.MethodPost, "/api/book", strings.NewReader(body)))

	if rec.Code != http.StatusBadRequest {
		t.Errorf("status = %d, want 400", rec.Code)
	}
	if called {
		t.Error("se llamó a EA con datos incompletos")
	}
}

// Sin teléfono (o sin ningún dígito) ⇒ 400 sin llamar a EA: es el dato que
// identifica al cliente. El email en cambio ya no es obligatorio.
func TestHandleBookRequiresTelefono(t *testing.T) {
	called := false
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called = true }))
	defer srv.Close()
	ea := &eaClient{base: srv.URL, http: srv.Client()}

	for _, body := range []string{
		`{"nombre":"Ana","apellidos":"López","serviceIds":[1],"date":"2026-07-01","time":"10:00"}`,
		`{"nombre":"Ana","apellidos":"López","telefono":"--","serviceIds":[1],"date":"2026-07-01","time":"10:00"}`,
	} {
		rec := httptest.NewRecorder()
		handleBook(ea)(rec, httptest.NewRequest(http.MethodPost, "/api/book", strings.NewReader(body)))
		if rec.Code != http.StatusBadRequest {
			t.Errorf("status = %d, want 400 (body %s)", rec.Code, body)
		}
	}
	if called {
		t.Error("se llamó a EA con datos incompletos")
	}
}
