import { describe, it, expect, vi, beforeEach, afterAll } from 'vitest';

// Prevent the module-level setInterval from running during tests
vi.useFakeTimers();

// Prevent real HTTP requests
vi.stubGlobal('fetch', vi.fn());

afterAll(() => vi.unstubAllGlobals());

import {
  estimateCondition,
  cacheKey,
  getWeather,
  getDetailedWeather,
  ApiError,
  type WeatherResult,
} from '../../../src/services/weatherService';

// ── estimateCondition ────────────────────────────────────────────────────────

describe('estimateCondition', () => {
  describe('heavy precipitation (precipMm > 5)', () => {
    it('returns Snow when temp <= 0', () => {
      expect(estimateCondition(0, 6)).toBe('Snow');
      expect(estimateCondition(-5, 10)).toBe('Snow');
    });

    it('returns Rain when temp > 0', () => {
      expect(estimateCondition(1, 6)).toBe('Rain');
      expect(estimateCondition(20, 50)).toBe('Rain');
    });

    it('boundary: precipMm = 5.01 and temp = 0 -> Snow', () => {
      expect(estimateCondition(0, 5.01)).toBe('Snow');
    });

    it('boundary: precipMm = 5 is NOT heavy (exactly 5, not > 5) -> falls through', () => {
      // precipMm = 5 fails the > 5 check, falls to > 1 check -> Snow or Drizzle
      expect(estimateCondition(0, 5)).toBe('Snow'); // > 1 and temp <= 0
      expect(estimateCondition(5, 5)).toBe('Drizzle'); // > 1 and temp > 0
    });
  });

  describe('moderate precipitation (precipMm > 1)', () => {
    it('returns Snow when temp <= 0', () => {
      expect(estimateCondition(0, 2)).toBe('Snow');
      expect(estimateCondition(-10, 1.5)).toBe('Snow');
    });

    it('returns Drizzle when temp > 0', () => {
      expect(estimateCondition(5, 2)).toBe('Drizzle');
      expect(estimateCondition(15, 3)).toBe('Drizzle');
    });
  });

  describe('light precipitation (precipMm > 0.3)', () => {
    it('returns Clouds regardless of temperature', () => {
      expect(estimateCondition(-5, 0.5)).toBe('Clouds');
      expect(estimateCondition(25, 0.5)).toBe('Clouds');
    });

    it('boundary: precipMm = 0.31 -> Clouds', () => {
      expect(estimateCondition(20, 0.31)).toBe('Clouds');
    });

    it('boundary: precipMm = 0.3 is NOT light precipitation -> falls through', () => {
      // precipMm = 0.3 fails the > 0.3 check, falls to temperature check
      expect(estimateCondition(20, 0.3)).toBe('Clear'); // temp > 15
      expect(estimateCondition(10, 0.3)).toBe('Clouds'); // temp <= 15
    });
  });

  describe('dry conditions (precipMm <= 0.3)', () => {
    it('returns Clear when temp > 15', () => {
      expect(estimateCondition(16, 0)).toBe('Clear');
      expect(estimateCondition(30, 0.1)).toBe('Clear');
    });

    it('returns Clouds when temp <= 15', () => {
      expect(estimateCondition(15, 0)).toBe('Clouds');
      expect(estimateCondition(10, 0)).toBe('Clouds');
      expect(estimateCondition(-5, 0)).toBe('Clouds');
    });

    it('boundary: temp = 15 -> Clouds (not > 15)', () => {
      expect(estimateCondition(15, 0)).toBe('Clouds');
    });
  });
});

// ── cacheKey ─────────────────────────────────────────────────────────────────

describe('cacheKey', () => {
  it('rounds lat and lng to 2 decimal places', () => {
    expect(cacheKey('48.8566', '2.3522', '2024-06-15')).toBe('48.86_2.35_2024-06-15');
  });

  it('uses "current" when date is undefined', () => {
    expect(cacheKey('10.0', '20.0')).toBe('10.00_20.00_current');
  });

  it('handles negative coordinates', () => {
    expect(cacheKey('-33.8688', '151.2093', '2024-01-01')).toBe('-33.87_151.21_2024-01-01');
  });

  it('pads to 2 decimal places for round numbers', () => {
    expect(cacheKey('48', '2', '2024-01-01')).toBe('48.00_2.00_2024-01-01');
  });

  it('preserves the date string as-is', () => {
    expect(cacheKey('0', '0', 'climate')).toBe('0.00_0.00_climate');
  });
});

// ── Helpers ───────────────────────────────────────────────────────────────────

/** Build a minimal mock Response for fetch. */
function mockResponse(body: unknown, ok = true, status = 200): Response {
  return {
    ok,
    status,
    json: vi.fn().mockResolvedValue(body),
  } as unknown as Response;
}

/** ISO date string offset by `days` from now (fake-timer "now"). */
function dateOffset(days: number): string {
  const d = new Date(Date.now() + days * 24 * 60 * 60 * 1000);
  return d.toISOString().slice(0, 10);
}

// ── getWeather ────────────────────────────────────────────────────────────────

describe('getWeather', () => {
  // Use coordinates that are unique per describe block to avoid cross-test cache
  // pollution. Each nested describe uses a distinct lat so the module-level Map
  // never returns stale data from a sibling test.

  beforeEach(() => {
    vi.mocked(fetch).mockReset();
  });

  describe('with date — cache hit', () => {
    it('returns cached result without calling fetch', async () => {
      const date = dateOffset(2);
      const forecastBody = {
        daily: {
          time: [date],
          temperature_2m_max: [20],
          temperature_2m_min: [10],
          weathercode: [0],
        },
      };
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse(forecastBody));

      // First call populates the cache
      const first = await getWeather('10.00', '20.00', date, 'en');
      expect(fetch).toHaveBeenCalledTimes(1);

      vi.mocked(fetch).mockReset();

      // Second call with identical arguments should be served from cache
      const second = await getWeather('10.00', '20.00', date, 'en');
      expect(fetch).not.toHaveBeenCalled();
      expect(second).toEqual(first);
    });
  });

  describe('with date — forecast path (diffDays -1 .. +16)', () => {
    it('returns a forecast WeatherResult for a date 3 days away', async () => {
      const date = dateOffset(3);
      const body = {
        daily: {
          time: [date],
          temperature_2m_max: [25],
          temperature_2m_min: [15],
          weathercode: [1],
        },
      };
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse(body));

      const result = await getWeather('11.00', '21.00', date, 'en');

      expect(result.type).toBe('forecast');
      expect(result.temp).toBe(20); // (25+15)/2
      expect(result.temp_max).toBe(25);
      expect(result.temp_min).toBe(15);
      expect(result.main).toBe('Clear'); // WMO code 1
      expect(result.description).toBe('Mainly clear');
    });

    it('uses German descriptions when lang is "de"', async () => {
      const date = dateOffset(4);
      const body = {
        daily: {
          time: [date],
          temperature_2m_max: [10],
          temperature_2m_min: [5],
          weathercode: [3],
        },
      };
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse(body));

      const result = await getWeather('11.01', '21.01', date, 'de');

      expect(result.description).toBe('Bewolkt'); // German for code 3
    });

    it('falls back to "Clouds" for an unknown WMO code', async () => {
      const date = dateOffset(5);
      const body = {
        daily: {
          time: [date],
          temperature_2m_max: [10],
          temperature_2m_min: [5],
          weathercode: [999], // not in WMO_MAP
        },
      };
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse(body));

      const result = await getWeather('11.02', '21.02', date, 'en');

      expect(result.main).toBe('Clouds');
    });

    it('throws ApiError when response.ok is false', async () => {
      const date = dateOffset(2);
      const body = { reason: 'rate limited' };
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse(body, false, 429));
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse(body, false, 429));

      await expect(getWeather('12.00', '22.00', date, 'en')).rejects.toThrow(ApiError);
      await expect(getWeather('12.00', '22.00', date, 'en')).rejects.toMatchObject({
        status: 429,
        message: 'rate limited',
      });
    });

    it('throws ApiError when data.error is true', async () => {
      const date = dateOffset(2);
      const body = { error: true, reason: 'invalid coordinates' };
      // Need a fresh coordinate to avoid the cache from the previous test failure
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse(body, true, 200));

      await expect(getWeather('12.01', '22.01', date, 'en')).rejects.toThrow(ApiError);
    });

    it('falls through to climate path when date is not found in forecast data', async () => {
      // The forecast API returns data but NOT for our target date; the code
      // checks idx === -1 and falls into the diffDays > -1 climate branch.
      const date = dateOffset(3);
      const forecastBody = {
        daily: {
          time: ['1970-01-01'], // deliberately wrong date
          temperature_2m_max: [10],
          temperature_2m_min: [5],
          weathercode: [0],
        },
      };

      // Archive response for the climate fallback
      const refDate = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000);
      const archiveBody = {
        daily: {
          time: ['some-date'],
          temperature_2m_max: [18],
          temperature_2m_min: [8],
          precipitation_sum: [0],
        },
      };

      vi.mocked(fetch)
          .mockResolvedValueOnce(mockResponse(forecastBody))
          .mockResolvedValueOnce(mockResponse(archiveBody));

      const result = await getWeather('13.00', '23.00', date, 'en');

      expect(result.type).toBe('climate');
      expect(fetch).toHaveBeenCalledTimes(2);
    });
  });

  describe('with date — past date (diffDays < -1)', () => {
    it('returns forecast-type WeatherResult from the archive API', async () => {
      const date = dateOffset(-5); // 5 days in the past
      const archiveBody = {
        daily: {
          time: [date],
          temperature_2m_max: [18],
          temperature_2m_min: [10],
          weathercode: [2],
          precipitation_sum: [0],
        },
      };
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse(archiveBody));

      const result = await getWeather('14.00', '24.00', date, 'en');

      expect(result.type).toBe('forecast');
      expect(result.temp).toBe(14);
      expect(result.temp_max).toBe(18);
      expect(result.temp_min).toBe(10);
      expect(fetch).toHaveBeenCalledTimes(1);
      expect(vi.mocked(fetch).mock.calls[0][0]).toContain('archive-api.open-meteo.com');
    });

    it('returns no_forecast error when archive has no data for the date', async () => {
      const date = dateOffset(-5);
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse({ daily: { time: [], temperature_2m_max: [], temperature_2m_min: [], weathercode: [] } }));

      const result = await getWeather('14.01', '24.01', date, 'en');

      expect(result.error).toBe('no_forecast');
    });
  });

  describe('with date — climate / archive path (diffDays > 16)', () => {
    it('returns a climate WeatherResult for a far-future date', async () => {
      const date = dateOffset(20);
      const body = {
        daily: {
          time: ['2025-01-01', '2025-01-02'],
          temperature_2m_max: [22, 24],
          temperature_2m_min: [12, 14],
          precipitation_sum: [0, 0.1],
        },
      };
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse(body));

      const result = await getWeather('15.00', '25.00', date, 'en');

      expect(result.type).toBe('climate');
      expect(result.temp).toBe(18); // avg of (22+12)/2=17 and (24+14)/2=19 -> avg 18
      expect(result.temp_max).toBe(23);
      expect(result.temp_min).toBe(13);
    });

    it('throws ApiError when archive API response.ok is false', async () => {
      const date = dateOffset(20);
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse({ reason: 'server error' }, false, 500));
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse({ reason: 'server error' }, false, 500));

      await expect(getWeather('15.01', '25.01', date, 'en')).rejects.toThrow(ApiError);
      await expect(getWeather('15.01', '25.01', date, 'en')).rejects.toMatchObject({ status: 500 });
    });

    it('returns no_forecast when archive daily data is missing', async () => {
      const date = dateOffset(20);
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse({}));

      const result = await getWeather('15.02', '25.02', date, 'en');

      expect(result.error).toBe('no_forecast');
    });

    it('returns no_forecast when archive daily.time is empty', async () => {
      const date = dateOffset(20);
      const body = { daily: { time: [], temperature_2m_max: [], temperature_2m_min: [], precipitation_sum: [] } };
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse(body));

      const result = await getWeather('15.03', '25.03', date, 'en');

      expect(result.error).toBe('no_forecast');
    });

    it('returns no_forecast when all temperature entries are null', async () => {
      const date = dateOffset(20);
      const body = {
        daily: {
          time: ['2025-01-01'],
          temperature_2m_max: [null],
          temperature_2m_min: [null],
          precipitation_sum: [0],
        },
      };
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse(body));

      const result = await getWeather('15.04', '25.04', date, 'en');

      expect(result.error).toBe('no_forecast');
    });
  });

  describe('without date — current weather path', () => {
    it('returns current WeatherResult', async () => {
      const body = {
        current: { temperature_2m: 18.7, weathercode: 2 },
      };
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse(body));

      const result = await getWeather('16.00', '26.00', undefined, 'en');

      expect(result.type).toBe('current');
      expect(result.temp).toBe(19); // Math.round(18.7)
      expect(result.main).toBe('Clouds'); // WMO code 2
      expect(result.description).toBe('Partly cloudy');
    });

    it('uses German descriptions when lang is "de"', async () => {
      const body = { current: { temperature_2m: 10, weathercode: 45 } };
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse(body));

      const result = await getWeather('16.01', '26.01', undefined, 'de');

      expect(result.description).toBe('Nebel');
    });

    it('returns cached current weather on second identical call', async () => {
      const body = { current: { temperature_2m: 22, weathercode: 0 } };
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse(body));

      const first = await getWeather('16.02', '26.02', undefined, 'en');
      vi.mocked(fetch).mockReset();
      const second = await getWeather('16.02', '26.02', undefined, 'en');

      expect(fetch).not.toHaveBeenCalled();
      expect(second).toEqual(first);
    });

    it('throws ApiError when current weather API returns error', async () => {
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse({ reason: 'bad request' }, false, 400));
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse({ reason: 'bad request' }, false, 400));

      await expect(getWeather('16.03', '26.03', undefined, 'en')).rejects.toThrow(ApiError);
      await expect(getWeather('16.03', '26.03', undefined, 'en')).rejects.toMatchObject({ status: 400 });
    });

    it('throws ApiError when data.error flag is set on current weather response', async () => {
      const body = { error: true, reason: 'quota exceeded' };
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse(body, true, 200));

      await expect(getWeather('16.04', '26.04', undefined, 'en')).rejects.toThrow(ApiError);
    });
  });
});

// ── getDetailedWeather ────────────────────────────────────────────────────────

describe('getDetailedWeather', () => {
  beforeEach(() => {
    vi.mocked(fetch).mockReset();
  });

  describe('cache hit', () => {
    it('returns cached result without calling fetch a second time', async () => {
      const date = dateOffset(5);
      const dailyBody = {
        daily: {
          time: [date],
          temperature_2m_max: [28],
          temperature_2m_min: [18],
          weathercode: [0],
          precipitation_sum: [0],
          precipitation_probability_max: [0],
          windspeed_10m_max: [10],
          sunrise: [`${date}T06:00`],
          sunset: [`${date}T20:00`],
        },
        hourly: { time: [], temperature_2m: [] },
      };
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse(dailyBody));

      const first = await getDetailedWeather('30.00', '40.00', date, 'en');
      vi.mocked(fetch).mockReset();
      const second = await getDetailedWeather('30.00', '40.00', date, 'en');

      expect(fetch).not.toHaveBeenCalled();
      expect(second).toEqual(first);
    });
  });

  describe('forecast path (diffDays <= 16)', () => {
    it('returns a detailed forecast WeatherResult with hourly data', async () => {
      const date = dateOffset(6);
      const body = {
        daily: {
          time: [date],
          temperature_2m_max: [30],
          temperature_2m_min: [20],
          weathercode: [80],
          precipitation_sum: [5],
          precipitation_probability_max: [70],
          windspeed_10m_max: [15],
          sunrise: [`${date}T05:45`],
          sunset: [`${date}T21:15`],
        },
        hourly: {
          time: [`${date}T12:00`, `${date}T13:00`],
          temperature_2m: [28, 29],
          precipitation_probability: [60, 65],
          precipitation: [1.2, 0.8],
          weathercode: [80, 81],
          windspeed_10m: [12, 14],
          relativehumidity_2m: [70, 68],
        },
      };
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse(body));

      const result = await getDetailedWeather('31.00', '41.00', date, 'en');

      expect(result.type).toBe('forecast');
      expect(result.temp).toBe(25); // (30+20)/2
      expect(result.temp_max).toBe(30);
      expect(result.temp_min).toBe(20);
      expect(result.main).toBe('Rain'); // WMO code 80
      expect(result.precipitation_sum).toBe(5);
      expect(result.precipitation_probability_max).toBe(70);
      expect(result.wind_max).toBe(15);
      expect(result.sunrise).toBe('05:45');
      expect(result.sunset).toBe('21:15');
      expect(result.hourly).toHaveLength(2);
      expect(result.hourly![0].temp).toBe(28);
      expect(result.hourly![0].precipitation_probability).toBe(60);
      expect(result.hourly![1].main).toBe('Rain'); // WMO code 81
    });

    it('returns no_forecast when daily data is missing', async () => {
      const date = dateOffset(7);
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse({}));

      const result = await getDetailedWeather('31.01', '41.01', date, 'en');

      expect(result.error).toBe('no_forecast');
    });

    it('returns no_forecast when daily.time is empty', async () => {
      const date = dateOffset(7);
      const body = {
        daily: {
          time: [],
          temperature_2m_max: [],
          temperature_2m_min: [],
          weathercode: [],
        },
      };
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse(body));

      const result = await getDetailedWeather('31.02', '41.02', date, 'en');

      expect(result.error).toBe('no_forecast');
    });

    it('throws ApiError when forecast API returns !ok', async () => {
      const date = dateOffset(8);
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse({ reason: 'not found' }, false, 404));
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse({ reason: 'not found' }, false, 404));

      await expect(getDetailedWeather('31.03', '41.03', date, 'en')).rejects.toThrow(ApiError);
      await expect(getDetailedWeather('31.03', '41.03', date, 'en')).rejects.toMatchObject({ status: 404 });
    });

    it('throws ApiError when data.error flag is set', async () => {
      const date = dateOffset(9);
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse({ error: true, reason: 'bad coords' }));

      await expect(getDetailedWeather('31.04', '41.04', date, 'en')).rejects.toThrow(ApiError);
    });

    it('handles missing hourly block gracefully', async () => {
      const date = dateOffset(10);
      const body = {
        daily: {
          time: [date],
          temperature_2m_max: [20],
          temperature_2m_min: [10],
          weathercode: [0],
          precipitation_sum: [0],
          precipitation_probability_max: [0],
          windspeed_10m_max: [5],
          sunrise: [`${date}T06:00`],
          sunset: [`${date}T20:00`],
        },
        // no hourly field
      };
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse(body));

      const result = await getDetailedWeather('31.05', '41.05', date, 'en');

      expect(result.type).toBe('forecast');
      expect(result.hourly).toEqual([]);
    });
  });

  describe('climate / archive path (diffDays > 16)', () => {
    it('returns a detailed climate WeatherResult with hourly data', async () => {
      const date = dateOffset(20);
      const refDate = new Date(Date.now() + 20 * 24 * 60 * 60 * 1000);
      const refYear = refDate.getFullYear() - 1;
      const refDateStr = `${refYear}-${String(refDate.getMonth() + 1).padStart(2, '0')}-${String(refDate.getDate()).padStart(2, '0')}`;

      const body = {
        daily: {
          time: [refDateStr],
          temperature_2m_max: [26],
          temperature_2m_min: [16],
          weathercode: [63],
          precipitation_sum: [8],
          windspeed_10m_max: [20],
          sunrise: [`${refDateStr}T06:30`],
          sunset: [`${refDateStr}T20:30`],
        },
        hourly: {
          time: [`${refDateStr}T10:00`, `${refDateStr}T11:00`],
          temperature_2m: [22, 24],
          precipitation: [2, 1],
          weathercode: [63, 61],
          windspeed_10m: [18, 16],
          relativehumidity_2m: [80, 75],
        },
      };
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse(body));

      const result = await getDetailedWeather('32.00', '42.00', date, 'en');

      expect(result.type).toBe('climate');
      expect(result.temp).toBe(21); // (26+16)/2
      expect(result.temp_max).toBe(26);
      expect(result.temp_min).toBe(16);
      expect(result.main).toBe('Rain'); // WMO code 63
      expect(result.description).toBe('Rain'); // WMO_DESCRIPTION_EN[63]
      expect(result.precipitation_sum).toBe(8);
      expect(result.wind_max).toBe(20);
      expect(result.sunrise).toBe('06:30');
      expect(result.sunset).toBe('20:30');
      expect(result.hourly).toHaveLength(2);
      expect(result.hourly![0].temp).toBe(22);
      expect(result.hourly![0].precipitation).toBe(2);
      expect(result.hourly![1].main).toBe('Rain'); // WMO code 61
    });

    it('uses German descriptions when lang is "de"', async () => {
      const date = dateOffset(20);
      const refDate = new Date(Date.now() + 20 * 24 * 60 * 60 * 1000);
      const refYear = refDate.getFullYear() - 1;
      const refDateStr = `${refYear}-${String(refDate.getMonth() + 1).padStart(2, '0')}-${String(refDate.getDate()).padStart(2, '0')}`;

      const body = {
        daily: {
          time: [refDateStr],
          temperature_2m_max: [20],
          temperature_2m_min: [10],
          weathercode: [0],
          precipitation_sum: [0],
          windspeed_10m_max: [5],
        },
        hourly: { time: [], temperature_2m: [] },
      };
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse(body));

      const result = await getDetailedWeather('32.01', '42.01', date, 'de');

      expect(result.description).toBe('Klar'); // German WMO_DESCRIPTION_DE[0]
    });

    it('returns no_forecast when archive daily data is missing', async () => {
      const date = dateOffset(20);
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse({}));

      const result = await getDetailedWeather('32.02', '42.02', date, 'en');

      expect(result.error).toBe('no_forecast');
    });

    it('returns no_forecast when archive daily.time is empty', async () => {
      const date = dateOffset(20);
      const body = { daily: { time: [], temperature_2m_max: [], temperature_2m_min: [], weathercode: [] } };
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse(body));

      const result = await getDetailedWeather('32.03', '42.03', date, 'en');

      expect(result.error).toBe('no_forecast');
    });

    it('throws ApiError when archive API returns !ok', async () => {
      const date = dateOffset(20);
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse({ reason: 'upstream error' }, false, 503));
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse({ reason: 'upstream error' }, false, 503));

      await expect(getDetailedWeather('32.04', '42.04', date, 'en')).rejects.toThrow(ApiError);
      await expect(getDetailedWeather('32.04', '42.04', date, 'en')).rejects.toMatchObject({ status: 503 });
    });

    it('throws ApiError when archive data.error flag is set', async () => {
      const date = dateOffset(20);
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse({ error: true, reason: 'quota exceeded' }));

      await expect(getDetailedWeather('32.05', '42.05', date, 'en')).rejects.toThrow(ApiError);
    });

    it('falls back to estimateCondition when archive weathercode is undefined', async () => {
      // When daily.weathercode[0] is undefined, the code falls back to
      // estimateCondition(avgTemp, precipitation_sum)
      const date = dateOffset(20);
      const refDate = new Date(Date.now() + 20 * 24 * 60 * 60 * 1000);
      const refYear = refDate.getFullYear() - 1;
      const refDateStr = `${refYear}-${String(refDate.getMonth() + 1).padStart(2, '0')}-${String(refDate.getDate()).padStart(2, '0')}`;

      const body = {
        daily: {
          time: [refDateStr],
          temperature_2m_max: [20],
          temperature_2m_min: [10],
          // weathercode intentionally omitted — will be undefined
          precipitation_sum: [10], // > 5 mm and temp > 0 -> 'Rain'
          windspeed_10m_max: [5],
        },
        hourly: { time: [], temperature_2m: [] },
      };
      vi.mocked(fetch).mockResolvedValueOnce(mockResponse(body));

      const result = await getDetailedWeather('32.06', '42.06', date, 'en');

      // undefined code -> WMO_MAP[undefined] is undefined -> falls back to estimateCondition
      // avgTemp = (20+10)/2 = 15, precip = 10 > 5 and temp 15 > 0 -> 'Rain'
      expect(result.main).toBe('Rain');
    });
  });
});
