// test/unit/geofence_calculation_test.dart
//
// Unit test untuk GeofenceCalculator — Haversine formula & zone check.
// Jalankan: flutter test test/unit/geofence_calculation_test.dart
//
// Pure Dart test — tidak ada dependency eksternal.

import 'dart:math' as math;
import 'package:flutter_test/flutter_test.dart';

// ---------- GeofenceCalculator (mirror logika dari project) ----------

class GeofenceCalculator {
  static const double _earthRadiusMeters = 6371000.0;

  /// Hitung jarak (meter) antara dua koordinat GPS menggunakan Haversine formula.
  double haversineDistance(
    double lat1,
    double lng1,
    double lat2,
    double lng2,
  ) {
    final dLat = _toRad(lat2 - lat1);
    final dLng = _toRad(lng2 - lng1);

    final a = math.sin(dLat / 2) * math.sin(dLat / 2) +
        math.cos(_toRad(lat1)) *
            math.cos(_toRad(lat2)) *
            math.sin(dLng / 2) *
            math.sin(dLng / 2);

    final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
    return _earthRadiusMeters * c;
  }

  /// Return true jika koordinat berada di dalam radius dari center.
  bool isInsideGeofence({
    required double centerLat,
    required double centerLng,
    required double radiusMeters,
    required double currentLat,
    required double currentLng,
  }) {
    final dist =
        haversineDistance(centerLat, centerLng, currentLat, currentLng);
    return dist <= radiusMeters;
  }

  /// Return true jika user baru saja keluar dari geofence.
  bool hasExitedGeofence({
    required double centerLat,
    required double centerLng,
    required double radiusMeters,
    required double previousLat,
    required double previousLng,
    required double currentLat,
    required double currentLng,
  }) {
    final wasInside = isInsideGeofence(
      centerLat: centerLat,
      centerLng: centerLng,
      radiusMeters: radiusMeters,
      currentLat: previousLat,
      currentLng: previousLng,
    );
    final isInside = isInsideGeofence(
      centerLat: centerLat,
      centerLng: centerLng,
      radiusMeters: radiusMeters,
      currentLat: currentLat,
      currentLng: currentLng,
    );
    return wasInside && !isInside;
  }

  /// Return true jika user baru saja masuk ke geofence.
  bool hasEnteredGeofence({
    required double centerLat,
    required double centerLng,
    required double radiusMeters,
    required double previousLat,
    required double previousLng,
    required double currentLat,
    required double currentLng,
  }) {
    final wasInside = isInsideGeofence(
      centerLat: centerLat,
      centerLng: centerLng,
      radiusMeters: radiusMeters,
      currentLat: previousLat,
      currentLng: previousLng,
    );
    final isInside = isInsideGeofence(
      centerLat: centerLat,
      centerLng: centerLng,
      radiusMeters: radiusMeters,
      currentLat: currentLat,
      currentLng: currentLng,
    );
    return !wasInside && isInside;
  }

  double _toRad(double deg) => deg * math.pi / 180.0;
}

// ---------- Koordinat referensi ----------
// ITS (Institut Teknologi Sepuluh Nopember) Surabaya
const double _itsCenterLat = -7.2754;
const double _itsCenterLng = 112.7920;

// ---------- Tests ----------

void main() {
  late GeofenceCalculator calc;

  setUp(() {
    calc = GeofenceCalculator();
  });

  // ── HAVERSINE DISTANCE TESTS ─────────────────────────────────────

  group('haversineDistance — akurasi jarak', () {
    test('jarak titik yang sama → 0 meter', () {
      final dist = calc.haversineDistance(
          _itsCenterLat, _itsCenterLng, _itsCenterLat, _itsCenterLng);
      expect(dist, closeTo(0.0, 0.001));
    });

    test('jarak ~111 km per derajat lintang (ekuator)', () {
      // 1 derajat lintang ≈ 111 km
      final dist =
          calc.haversineDistance(0.0, 0.0, 1.0, 0.0); // 0° → 1°N di ekuator
      expect(dist, closeTo(111_195, 500)); // ±500 meter toleransi
    });

    test('jarak ~111 km per derajat bujur (di ekuator)', () {
      final dist = calc.haversineDistance(0.0, 0.0, 0.0, 1.0);
      expect(dist, closeTo(111_195, 500));
    });

    test('jarak ITS ke Tunjungan Plaza ≈ 7.6 km', () {
      // Tunjungan Plaza Surabaya: -7.2611, 112.7380
      const tpLat = -7.2611;
      const tpLng = 112.7380;
      final dist = calc.haversineDistance(
          _itsCenterLat, _itsCenterLng, tpLat, tpLng);
      // Jarak darat sekitar 7-8 km, Haversine (garis lurus) ≈ 5-6 km
      expect(dist, greaterThan(4000));
      expect(dist, lessThan(9000));
    });

    test('jarak simetris: A→B == B→A', () {
      const lat1 = -7.2754;
      const lng1 = 112.7920;
      const lat2 = -7.2611;
      const lng2 = 112.7380;
      final d1 = calc.haversineDistance(lat1, lng1, lat2, lng2);
      final d2 = calc.haversineDistance(lat2, lng2, lat1, lng1);
      expect(d1, closeTo(d2, 0.001));
    });

    test('jarak ≥ 0 untuk semua koordinat valid', () {
      final dist =
          calc.haversineDistance(-90.0, -180.0, 90.0, 180.0);
      expect(dist, greaterThanOrEqualTo(0));
    });
  });

  // ── IS INSIDE GEOFENCE ───────────────────────────────────────────

  group('isInsideGeofence — pengecekan zona', () {
    // Radius 500 meter dari ITS
    const radius = 500.0;

    test('titik tepat di center → inside', () {
      expect(
        calc.isInsideGeofence(
          centerLat: _itsCenterLat,
          centerLng: _itsCenterLng,
          radiusMeters: radius,
          currentLat: _itsCenterLat,
          currentLng: _itsCenterLng,
        ),
        isTrue,
      );
    });

    test('titik 100 meter dari center → inside radius 500m', () {
      // ±0.001° ≈ 100-111 meter
      expect(
        calc.isInsideGeofence(
          centerLat: _itsCenterLat,
          centerLng: _itsCenterLng,
          radiusMeters: radius,
          currentLat: _itsCenterLat + 0.0009,
          currentLng: _itsCenterLng,
        ),
        isTrue,
      );
    });

    test('titik 600 meter dari center → outside radius 500m', () {
      // ±0.006° ≈ 600+ meter
      expect(
        calc.isInsideGeofence(
          centerLat: _itsCenterLat,
          centerLng: _itsCenterLng,
          radiusMeters: radius,
          currentLat: _itsCenterLat + 0.006,
          currentLng: _itsCenterLng,
        ),
        isFalse,
      );
    });

    test('radius sangat kecil (10m) — hanya titik sangat dekat yang inside',
        () {
      // ±0.0001° ≈ 11 meter
      expect(
        calc.isInsideGeofence(
          centerLat: _itsCenterLat,
          centerLng: _itsCenterLng,
          radiusMeters: 10,
          currentLat: _itsCenterLat + 0.0001,
          currentLng: _itsCenterLng,
        ),
        isFalse,
      );
    });

    test('radius sangat besar (10km) — titik jauh masih inside', () {
      expect(
        calc.isInsideGeofence(
          centerLat: _itsCenterLat,
          centerLng: _itsCenterLng,
          radiusMeters: 10000,
          currentLat: _itsCenterLat + 0.05,
          currentLng: _itsCenterLng,
        ),
        isTrue,
      );
    });
  });

  // ── GEOFENCE EXIT DETECTION ───────────────────────────────────────

  group('hasExitedGeofence', () {
    const centerLat = _itsCenterLat;
    const centerLng = _itsCenterLng;
    const radius = 500.0;
    // Koordinat dalam radius
    const insideLat = _itsCenterLat + 0.001;  // ≈ 111m
    const insideLng = _itsCenterLng;
    // Koordinat di luar radius
    const outsideLat = _itsCenterLat + 0.01; // ≈ 1110m
    const outsideLng = _itsCenterLng;

    test('pindah dari dalam ke luar → hasExited = true', () {
      expect(
        calc.hasExitedGeofence(
          centerLat: centerLat,
          centerLng: centerLng,
          radiusMeters: radius,
          previousLat: insideLat,
          previousLng: insideLng,
          currentLat: outsideLat,
          currentLng: outsideLng,
        ),
        isTrue,
      );
    });

    test('masih di dalam → hasExited = false', () {
      expect(
        calc.hasExitedGeofence(
          centerLat: centerLat,
          centerLng: centerLng,
          radiusMeters: radius,
          previousLat: insideLat,
          previousLng: insideLng,
          currentLat: insideLat + 0.0005,
          currentLng: insideLng,
        ),
        isFalse,
      );
    });

    test('sudah di luar dan makin jauh → hasExited = false', () {
      expect(
        calc.hasExitedGeofence(
          centerLat: centerLat,
          centerLng: centerLng,
          radiusMeters: radius,
          previousLat: outsideLat,
          previousLng: outsideLng,
          currentLat: outsideLat + 0.005,
          currentLng: outsideLng,
        ),
        isFalse,
      );
    });
  });

  // ── GEOFENCE ENTER DETECTION ─────────────────────────────────────

  group('hasEnteredGeofence', () {
    const centerLat = _itsCenterLat;
    const centerLng = _itsCenterLng;
    const radius = 500.0;
    const insideLat = _itsCenterLat + 0.001;
    const insideLng = _itsCenterLng;
    const outsideLat = _itsCenterLat + 0.01;
    const outsideLng = _itsCenterLng;

    test('pindah dari luar ke dalam → hasEntered = true', () {
      expect(
        calc.hasEnteredGeofence(
          centerLat: centerLat,
          centerLng: centerLng,
          radiusMeters: radius,
          previousLat: outsideLat,
          previousLng: outsideLng,
          currentLat: insideLat,
          currentLng: insideLng,
        ),
        isTrue,
      );
    });

    test('sudah di dalam → hasEntered = false', () {
      expect(
        calc.hasEnteredGeofence(
          centerLat: centerLat,
          centerLng: centerLng,
          radiusMeters: radius,
          previousLat: insideLat,
          previousLng: insideLng,
          currentLat: insideLat + 0.0001,
          currentLng: insideLng,
        ),
        isFalse,
      );
    });
  });

  // ── REAL WORLD REFERENCE TESTS ────────────────────────────────────

  group('real-world reference distances', () {
    test('jarak ITS ke Surabaya Gubeng station ≈ 6-8 km', () {
      // Stasiun Gubeng: -7.2651, 112.7509
      final dist = calc.haversineDistance(
          _itsCenterLat, _itsCenterLng, -7.2651, 112.7509);
      expect(dist, greaterThan(4000));
      expect(dist, lessThan(10000));
    });

    test('jarak Jakarta ke Surabaya ≈ 700-800 km', () {
      // Jakarta: -6.2088, 106.8456
      // Surabaya: -7.2575, 112.7521
      final dist =
          calc.haversineDistance(-6.2088, 106.8456, -7.2575, 112.7521);
      expect(dist, greaterThan(650_000));
      expect(dist, lessThan(850_000));
    });
  });
}