package weather

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

func TestOpenMeteoProvider(t *testing.T) {
	var geoReqCount, weatherReqCount int
	mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/v1/search" {
			geoReqCount++
			name, lat, lon := "Moscow", 55.7558, 37.6173
			json.NewEncoder(w).Encode(geoResponse{
				Results: []geoResult{{
					Name: &name, Latitude: &lat, Longitude: &lon, Country: "Russia",
				}},
			})
			return
		}
		if r.URL.Path == "/v1/forecast" {
			weatherReqCount++
			json.NewEncoder(w).Encode(validForecast(22.5, 0, 3.2))
			return
		}
		w.WriteHeader(http.StatusNotFound)
	}))
	defer mock.Close()

	p := NewOpenMeteoProvider()
	transport := &mockTransport{
		geoURL:      mock.URL + "/v1/search",
		forecastURL: mock.URL + "/v1/forecast",
	}
	p.SetClient(&http.Client{Transport: transport})

	w, err := p.CurrentWeather(context.Background(), "Moscow")
	if err != nil {
		t.Fatalf("CurrentWeather: %v", err)
	}
	if w.Location != "Moscow" {
		t.Errorf("location = %q, want Moscow", w.Location)
	}
	if w.Temperature != 22.5 {
		t.Errorf("temperature = %f, want 22.5", w.Temperature)
	}
	if w.Condition != "ясно" {
		t.Errorf("condition = %q, want ясно", w.Condition)
	}
}

func validForecast(temperature float64, code int, wind float64) forecastResponse {
	return forecastResponse{CurrentWeather: &currentWeather{
		Temperature: &temperature,
		WMO:         &code,
		WindSpeed:   &wind,
	}}
}

func validGeo(name string, lat, lon float64) geoResponse {
	return geoResponse{Results: []geoResult{{Name: &name, Latitude: &lat, Longitude: &lon, Country: "Russia"}}}
}

type mockTransport struct {
	geoURL      string
	forecastURL string
}

func (t *mockTransport) RoundTrip(r *http.Request) (*http.Response, error) {
	var targetURL string
	if r.URL.Host == "geocoding-api.open-meteo.com" {
		targetURL = t.geoURL + "?" + r.URL.RawQuery
	} else if r.URL.Host == "api.open-meteo.com" {
		targetURL = t.forecastURL + "?" + r.URL.RawQuery
	} else {
		return nil, fmt.Errorf("unexpected host: %s", r.URL.Host)
	}
	req, _ := http.NewRequestWithContext(r.Context(), r.Method, targetURL, nil)
	return http.DefaultTransport.RoundTrip(req)
}

// TestOpenMeteoProvider_ForecastHTTPError — a non-200 forecast reply must not
// decode into a zero-value Weather{Temperature: 0, Condition: "неизвестно"}
// reported as success (Vikunja #589). It must be a distinct error, not
// ErrLocationUnknown, so the caller does not blame the owner's city.
func TestOpenMeteoProvider_ForecastHTTPError(t *testing.T) {
	mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/v1/search" {
			json.NewEncoder(w).Encode(validGeo("Moscow", 55.7558, 37.6173))
			return
		}
		w.WriteHeader(http.StatusInternalServerError)
	}))
	defer mock.Close()

	p := NewOpenMeteoProvider()
	p.SetClient(&http.Client{Transport: &mockTransport{
		geoURL:      mock.URL + "/v1/search",
		forecastURL: mock.URL + "/v1/forecast",
	}})

	w, err := p.CurrentWeather(context.Background(), "Moscow")
	if err == nil {
		t.Fatalf("CurrentWeather: want error, got Weather{%+v}", w)
	}
	if errors.Is(err, ErrLocationUnknown) {
		t.Fatalf("CurrentWeather: want a service error, got ErrLocationUnknown: %v", err)
	}
	if !strings.Contains(err.Error(), "500") {
		t.Errorf("CurrentWeather: error %q does not name the status", err.Error())
	}
}

func TestOpenMeteoProviderRejectsIncompleteOrInvalidForecast(t *testing.T) {
	tests := []struct {
		name string
		body string
	}{
		{name: "empty object", body: `{}`},
		{name: "empty current weather", body: `{"current_weather":{}}`},
		{name: "missing wind", body: `{"current_weather":{"temperature":12,"weathercode":1}}`},
		{name: "temperature range", body: `{"current_weather":{"temperature":100,"weathercode":1,"windspeed":3}}`},
		{name: "weather code range", body: `{"current_weather":{"temperature":12,"weathercode":100,"windspeed":3}}`},
		{name: "wind range", body: `{"current_weather":{"temperature":12,"weathercode":1,"windspeed":-1}}`},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				if r.URL.Path == "/v1/search" {
					_ = json.NewEncoder(w).Encode(validGeo("Moscow", 55.7558, 37.6173))
					return
				}
				_, _ = w.Write([]byte(tc.body))
			}))
			defer mock.Close()
			p := NewOpenMeteoProvider()
			p.SetClient(&http.Client{Transport: &mockTransport{
				geoURL: mock.URL + "/v1/search", forecastURL: mock.URL + "/v1/forecast",
			}})
			if got, err := p.CurrentWeather(context.Background(), "Moscow"); err == nil {
				t.Fatalf("invalid forecast returned %+v", got)
			}
		})
	}
}

func TestOpenMeteoProviderBoundsBothResponseBodies(t *testing.T) {
	for _, endpoint := range []string{"geocode", "forecast"} {
		t.Run(endpoint, func(t *testing.T) {
			mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				oversize := endpoint == "geocode" && r.URL.Path == "/v1/search" ||
					endpoint == "forecast" && r.URL.Path == "/v1/forecast"
				if oversize {
					_, _ = w.Write([]byte(strings.Repeat("x", int(maxResponseBytes+1))))
					return
				}
				if r.URL.Path == "/v1/search" {
					_ = json.NewEncoder(w).Encode(validGeo("Moscow", 55.7558, 37.6173))
				} else {
					_ = json.NewEncoder(w).Encode(validForecast(12, 1, 3))
				}
			}))
			defer mock.Close()
			p := NewOpenMeteoProvider()
			p.SetClient(&http.Client{Transport: &mockTransport{
				geoURL: mock.URL + "/v1/search", forecastURL: mock.URL + "/v1/forecast",
			}})
			if _, err := p.CurrentWeather(context.Background(), "Moscow"); err == nil || !strings.Contains(err.Error(), "response exceeds") {
				t.Fatalf("oversize %s error = %v", endpoint, err)
			}
		})
	}
}

// TestOpenMeteoProvider_GeocodeHTTPError — a non-200 geocode reply must not be
// read as "no such city" (ErrLocationUnknown). A 500 is a service outage, and
// the owner hears a different sentence for each (Vikunja #589).
func TestOpenMeteoProvider_GeocodeHTTPError(t *testing.T) {
	mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusInternalServerError)
	}))
	defer mock.Close()

	p := NewOpenMeteoProvider()
	p.SetClient(&http.Client{Transport: &mockTransport{
		geoURL:      mock.URL + "/v1/search",
		forecastURL: mock.URL + "/v1/forecast",
	}})

	_, err := p.CurrentWeather(context.Background(), "Уфе")
	if err == nil {
		t.Fatal("CurrentWeather: want error")
	}
	if errors.Is(err, ErrLocationUnknown) {
		t.Fatalf("CurrentWeather: want a service error, got ErrLocationUnknown: %v", err)
	}
	if !strings.Contains(err.Error(), "500") {
		t.Errorf("CurrentWeather: error %q does not name the status", err.Error())
	}
}

// TestOpenMeteoProvider_GeocodeEmptyResult — a genuine 200 reply with no
// results is still ErrLocationUnknown, distinct from a service failure.
func TestOpenMeteoProvider_GeocodeEmptyResult(t *testing.T) {
	mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		json.NewEncoder(w).Encode(geoResponse{Results: nil})
	}))
	defer mock.Close()

	p := NewOpenMeteoProvider()
	p.SetClient(&http.Client{Transport: &mockTransport{
		geoURL:      mock.URL + "/v1/search",
		forecastURL: mock.URL + "/v1/forecast",
	}})

	_, err := p.CurrentWeather(context.Background(), "Атлантида")
	if !errors.Is(err, ErrLocationUnknown) {
		t.Fatalf("CurrentWeather: want ErrLocationUnknown, got %v", err)
	}
}

func TestStubProvider(t *testing.T) {
	p := NewStubProvider()
	_, err := p.CurrentWeather(context.Background(), "Moscow")
	if err != ErrNotConfigured {
		t.Fatalf("StubProvider: want ErrNotConfigured, got %v", err)
	}
}

// TestLocationCandidates — he speaks the prepositional case and the geocoder
// wants the nominative (Vikunja #421).
//
// The dictionary answers before the reversals now, so the nominative it knows
// comes second and anything derived by hand follows (V-530). "Уфе" used to fall
// under a four-rune floor and was asked as spoken, so "Уфа" was never tried.
func TestLocationCandidates(t *testing.T) {
	cases := map[string][]string{
		"Москве":  {"Москве", "Москва", "Москв"},
		"Казани":  {"Казани", "Казань", "Казан"},
		"Лондоне": {"Лондоне", "Лондон", "Лондона"},
		"Тбилиси": {"Тбилиси", "Тбились", "Тбилис"},
		"Berlin":  {"Berlin"},
		"Уфе":     {"Уфе", "Уфа", "Уф"},
		// The dictionary does not know it, so the soft-sign reversal answers.
		"Твери": {"Твери", "Тверь", "Твер"},
	}
	for in, want := range cases {
		got := locationCandidates(in)
		if len(got) != len(want) {
			t.Errorf("locationCandidates(%q) = %v, want %v", in, got, want)
			continue
		}
		for i := range got {
			if got[i] != want[i] {
				t.Errorf("locationCandidates(%q) = %v, want %v", in, got, want)
				break
			}
		}
	}
}
