package org.whoneedhelp.mobile;

import static androidx.test.espresso.web.assertion.WebViewAssertions.webMatches;
import static androidx.test.espresso.web.sugar.Web.onWebView;
import static androidx.test.espresso.web.webdriver.DriverAtoms.findElement;
import static androidx.test.espresso.web.webdriver.DriverAtoms.getText;
import static androidx.test.espresso.web.webdriver.DriverAtoms.webClick;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import android.Manifest;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.SystemClock;
import android.service.notification.StatusBarNotification;

import androidx.test.core.app.ActivityScenario;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject2;
import androidx.test.uiautomator.Until;
import androidx.test.espresso.web.webdriver.Locator;

import org.junit.After;
import org.junit.Before;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

@RunWith(AndroidJUnit4.class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public final class AndroidClientInstrumentedTest {
    private static final String LOCATION_PERMISSION_CONTROLLER =
        "com.android.permissioncontroller";
    private static final long UI_TIMEOUT_MS = 10_000;

    private Context context;
    private UiDevice device;
    private FixtureHttpServer server;

    @Before
    public void setUp() throws Exception {
        context = ApplicationProvider.getApplicationContext();
        device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
        context.stopService(new Intent(context, TrackingService.class));
        context.getSystemService(NotificationManager.class).cancelAll();
        server = new FixtureHttpServer();
    }

    @After
    public void tearDown() throws Exception {
        context.stopService(new Intent(context, TrackingService.class));
        context.getSystemService(NotificationManager.class).cancelAll();
        device.pressBack();
        server.close();
    }

    @Test
    public void test01TrackingWithoutLocationPermissionStopsLocally() throws Exception {
        assertFalse(hasLocationPermission());

        try (ActivityScenario<MainActivity> scenario = launch("/permission-boundary")) {
            scenario.onActivity(activity -> {
                Intent intent = TrackingService.startIntent(
                    activity,
                    UUID.randomUUID().toString(),
                    "csrf-without-permission",
                    "_session=without-permission"
                );

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    activity.startForegroundService(intent);
                } else {
                    activity.startService(intent);
                }
            });

            waitForServiceState(false);
            assertFalse(
                device.hasObject(By.text(context.getString(R.string.tracking_active)))
            );
        }
    }

    @Test
    public void test02WebGeolocationRequestsAndGrantsRuntimePermission() throws Exception {
        assertFalse(hasLocationPermission());

        try (
            ActivityScenario<MainActivity> scenario = launch("/location-permission")
        ) {
            onWebView()
                .withElement(findElement(Locator.ID, "location"))
                .perform(webClick());

            UiObject2 allow = null;

            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
                allow = device.wait(
                    Until.findObject(
                        By.res(
                            "com.android.packageinstaller",
                            "permission_allow_button"
                        )
                    ),
                    UI_TIMEOUT_MS
                );
            }

            if (allow == null && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
                allow = device.wait(
                    Until.findObject(
                        By.res(
                            "com.google.android.packageinstaller",
                            "permission_allow_button"
                        )
                    ),
                    UI_TIMEOUT_MS
                );
            }

            if (allow == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                allow = device.wait(
                    Until.findObject(
                        By.res(
                            LOCATION_PERMISSION_CONTROLLER,
                            "permission_allow_foreground_only_button"
                        )
                    ),
                    UI_TIMEOUT_MS
                );
            }

            if (allow == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                allow = device.wait(
                    Until.findObject(By.textContains("While using the app")),
                    UI_TIMEOUT_MS
                );
            }

            assertNotNull("Android location permission prompt was not shown", allow);
            allow.click();

            waitForLocationPermission();
            assertNotNull(
                "WebView geolocation did not report a granted position",
                server.awaitRequestEndingWith(
                    "/geolocation-granted",
                    15,
                    TimeUnit.SECONDS
                )
            );
            onWebView()
                .withElement(findElement(Locator.ID, "marker"))
                .check(webMatches(getText(), containsString("location-granted")));
        }
    }

    @Test
    public void test03SameOriginDeepLinkRouteSurvivesRecreation() {
        try (ActivityScenario<MainActivity> scenario = launch("/requests/deep-link-check")) {
            onWebView()
                .withElement(findElement(Locator.ID, "marker"))
                .check(
                    webMatches(
                        getText(),
                        containsString("loaded:/requests/deep-link-check")
                    )
                );

            scenario.recreate();

            onWebView()
                .withElement(findElement(Locator.ID, "marker"))
                .check(
                    webMatches(
                        getText(),
                        containsString("loaded:/requests/deep-link-check")
                    )
                );
        }
    }

    @Test
    public void test04ForegroundTrackingPostsLocationAndNotificationStop() throws Exception {
        grantTrackingPermissions();
        String assignmentId = UUID.randomUUID().toString();

        try (ActivityScenario<MainActivity> scenario = launch("/tracking-success")) {
            startTracking(scenario, assignmentId);
            waitForServiceState(true);

            FixtureHttpServer.RecordedRequest position = server.awaitRequestEndingWith(
                "/mobile/tracking/" + assignmentId + "/position",
                15,
                TimeUnit.SECONDS
            );
            assertNotNull("Foreground service did not upload an emulator location", position);
            assertTrue(position.body.contains("\"latitude\":"));
            assertTrue(position.body.contains("\"longitude\":"));

            openNotificationAndClickStop();

            FixtureHttpServer.RecordedRequest stop = server.awaitRequestEndingWith(
                "/mobile/tracking/" + assignmentId + "/stop",
                10,
                TimeUnit.SECONDS
            );
            assertNotNull("Notification Stop did not call the server boundary", stop);
            waitForServiceState(false);
            assertTrue(
                device.wait(
                    Until.gone(By.text(context.getString(R.string.tracking_active))),
                    UI_TIMEOUT_MS
                )
            );
        }
    }

    @Test
    public void test05NetworkFailureKeepsVisibleRetryState() throws Exception {
        grantTrackingPermissions();
        server.disconnectStopRequest();
        String assignmentId = UUID.randomUUID().toString();

        try (ActivityScenario<MainActivity> scenario = launch("/tracking-network-failure")) {
            startTracking(scenario, assignmentId);
            waitForServiceState(true);
            openNotificationAndClickStop();

            FixtureHttpServer.RecordedRequest stop = server.awaitRequestEndingWith(
                "/mobile/tracking/" + assignmentId + "/stop",
                10,
                TimeUnit.SECONDS
            );
            assertNotNull("The disconnected Stop request did not reach the fixture", stop);
            assertTrue(
                device.wait(
                    Until.hasObject(
                        By.text(context.getString(R.string.tracking_stop_failed))
                    ),
                    UI_TIMEOUT_MS
                )
            );
            assertTrue(serviceIsRunning());
        }
    }

    @Test
    public void test06TrackingSurvivesHomeAndActivityDestruction() throws Exception {
        grantTrackingPermissions();
        String assignmentId = UUID.randomUUID().toString();
        String positionPath = "/mobile/tracking/" + assignmentId + "/position";
        ActivityScenario<MainActivity> scenario = launch("/tracking-lifecycle");

        try {
            startTracking(scenario, assignmentId);
            waitForServiceState(true);
            assertNotNull(
                "Foreground service did not upload its first location",
                server.awaitRequestEndingWith(positionPath, 15, TimeUnit.SECONDS)
            );

            int foregroundCount = server.requestCountEndingWith(positionPath);
            device.pressHome();

            assertNotNull(
                "Tracking stopped after the Activity left the foreground",
                server.awaitRequestAfterCount(
                    positionPath,
                    foregroundCount,
                    15,
                    TimeUnit.SECONDS
                )
            );
            assertTrue(serviceIsRunning());

            int stoppedActivityCount = server.requestCountEndingWith(positionPath);
            scenario.close();

            assertNotNull(
                "Tracking stopped after the Activity was destroyed",
                server.awaitRequestAfterCount(
                    positionPath,
                    stoppedActivityCount,
                    15,
                    TimeUnit.SECONDS
                )
            );
            assertTrue(serviceIsRunning());

            openNotificationAndClickStop();
            assertNotNull(
                "Notification Stop did not work after Activity destruction",
                server.awaitRequestEndingWith(
                    "/mobile/tracking/" + assignmentId + "/stop",
                    10,
                    TimeUnit.SECONDS
                )
            );
            waitForServiceState(false);
        } finally {
            scenario.close();
        }
    }

    @Test
    public void test07MainPageFailureHasWorkingRetryAction() throws Exception {
        server.disconnectNextGet("/retry-page");

        try (ActivityScenario<MainActivity> scenario = launch("/retry-page")) {
            assertTrue(
                "Main-page failure did not expose the recovery message",
                device.wait(
                    Until.hasObject(
                        By.text(context.getString(R.string.page_load_failed))
                    ),
                    UI_TIMEOUT_MS
                )
            );
            UiObject2 retry = device.wait(
                Until.findObject(By.res("android", "button1")),
                UI_TIMEOUT_MS
            );
            assertNotNull("Main-page failure did not expose a Retry action", retry);
            assertTrue(
                "The positive recovery action was not Retry",
                context.getString(R.string.retry).equalsIgnoreCase(retry.getText())
            );
            retry.click();

            onWebView()
                .withElement(findElement(Locator.ID, "marker"))
                .check(webMatches(getText(), containsString("loaded:/retry-page")));
        }
    }

    private ActivityScenario<MainActivity> launch(String path) {
        Intent intent = new Intent(
            Intent.ACTION_VIEW,
            Uri.parse(BuildConfig.BASE_URL + path),
            context,
            MainActivity.class
        );
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        return ActivityScenario.launch(intent);
    }

    private void startTracking(
        ActivityScenario<MainActivity> scenario,
        String assignmentId
    ) {
        scenario.onActivity(activity -> {
            Intent intent = TrackingService.startIntent(
                activity,
                assignmentId,
                "instrumentation-csrf",
                "_session=instrumentation"
            );

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                activity.startForegroundService(intent);
            } else {
                activity.startService(intent);
            }
        });
    }

    private void openNotificationAndClickStop() {
        device.openNotification();
        UiObject2 active = device.wait(
            Until.findObject(By.text(context.getString(R.string.tracking_active))),
            UI_TIMEOUT_MS
        );
        assertNotNull("Foreground tracking notification was not visible", active);

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            sendStopActionFromActiveNotification();
            return;
        }

        UiObject2 stop = device.wait(
            Until.findObject(By.text(context.getString(R.string.tracking_stop_action))),
            UI_TIMEOUT_MS
        );

        assertNotNull("Foreground tracking notification had no Stop action", stop);
        stop.click();
    }

    private void sendStopActionFromActiveNotification() {
        NotificationManager manager = context.getSystemService(NotificationManager.class);
        String expectedTitle = context.getString(R.string.tracking_stop_action);
        long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(UI_TIMEOUT_MS);

        while (System.nanoTime() < deadline) {
            for (StatusBarNotification statusBarNotification : manager.getActiveNotifications()) {
                Notification.Action[] actions = statusBarNotification.getNotification().actions;

                if (actions == null) {
                    continue;
                }

                for (Notification.Action action : actions) {
                    if (
                        action.title != null
                            && expectedTitle.contentEquals(action.title)
                            && action.actionIntent != null
                    ) {
                        try {
                            action.actionIntent.send();
                            return;
                        } catch (PendingIntent.CanceledException error) {
                            throw new AssertionError(
                                "Foreground tracking Stop action was canceled",
                                error
                            );
                        }
                    }
                }
            }

            SystemClock.sleep(50);
        }

        throw new AssertionError("Foreground tracking notification had no Stop action");
    }

    private boolean hasLocationPermission() {
        return context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
                == android.content.pm.PackageManager.PERMISSION_GRANTED
            || context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
                == android.content.pm.PackageManager.PERMISSION_GRANTED;
    }

    private void waitForLocationPermission() throws InterruptedException {
        long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(UI_TIMEOUT_MS);

        while (System.nanoTime() < deadline) {
            if (hasLocationPermission()) {
                return;
            }

            Thread.sleep(50);
        }

        assertTrue("Android location permission was not granted", hasLocationPermission());
    }

    private void grantTrackingPermissions() throws Exception {
        String packageName = context.getPackageName();
        device.executeShellCommand(
            "pm grant " + packageName + " " + Manifest.permission.ACCESS_FINE_LOCATION
        );

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            device.executeShellCommand(
                "pm grant " + packageName + " " + Manifest.permission.POST_NOTIFICATIONS
            );
        }

        assertTrue(hasLocationPermission());
    }

    private void waitForServiceState(boolean expected) throws Exception {
        long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10);

        while (System.nanoTime() < deadline) {
            if (serviceIsRunning() == expected) {
                return;
            }

            Thread.sleep(100);
        }

        assertTrue(
            "TrackingService running state did not become " + expected,
            serviceIsRunning() == expected
        );
    }

    private boolean serviceIsRunning() throws Exception {
        return device
            .executeShellCommand(
                "dumpsys activity services " + context.getPackageName()
            )
            .contains(TrackingService.class.getName());
    }

}
