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.clearElement;
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 androidx.test.espresso.web.webdriver.DriverAtoms.webKeys;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertTrue;

import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;

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

import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.concurrent.TimeUnit;

@RunWith(AndroidJUnit4.class)
public final class CrossClientStagingInstrumentedTest {
    private static final long PAGE_TIMEOUT_SECONDS = 45;

    private final Context context = ApplicationProvider.getApplicationContext();
    private final UiDevice device =
        UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
    private final Bundle arguments =
        InstrumentationRegistry.getArguments();

    @Test
    public void exchangeMessageAndTrackingWithBrowser() throws Exception {
        String loginPath = requiredArgument("login_path");
        String requestPath = requiredArgument("request_path");
        String androidMessage = requiredArgument("android_message");
        String browserReply = requiredArgument("browser_reply");

        try (ActivityScenario<MainActivity> scenario = launch(loginPath)) {
            waitForElement("remember-login-button");
            click("remember-login-button");
            waitForElementText(
                "main-content",
                "Help can be closer than you think."
            );
        }

        try (ActivityScenario<MainActivity> scenario = launch(requestPath)) {
            waitForSelector(".phx-connected");
            waitForElementText("messages", "No messages yet.");
            replaceText("message-body", androidMessage);
            click("send-message-button");
            waitForElementText("messages", androidMessage);
            click("share-location-button");
            waitForServiceState(true);
            waitForElementText("messages", browserReply);
            click("stop-location-button");
            waitForServiceState(false);
        }
    }

    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 String requiredArgument(String name) {
        String value = arguments.getString(name);

        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException(
                "Missing instrumentation argument: " + name
            );
        }

        return value;
    }

    private static void click(String id) throws InterruptedException {
        waitForElement(id);
        onWebView()
            .withElement(findElement(Locator.ID, id))
            .perform(webClick());
    }

    private static void replaceText(String id, String value)
        throws InterruptedException {
        waitForElement(id);
        onWebView()
            .withElement(findElement(Locator.ID, id))
            .perform(clearElement())
            .perform(webKeys(value));
    }

    private static void waitForElement(String id) throws InterruptedException {
        waitForElementText(id, "");
    }

    private static void waitForSelector(String selector)
        throws InterruptedException {
        long deadline =
            System.nanoTime() + TimeUnit.SECONDS.toNanos(PAGE_TIMEOUT_SECONDS);
        Throwable lastFailure = null;

        while (System.nanoTime() < deadline) {
            try {
                onWebView()
                    .withElement(findElement(Locator.CSS_SELECTOR, selector))
                    .check(webMatches(getText(), containsString("")));
                return;
            } catch (AssertionError | RuntimeException failure) {
                lastFailure = failure;
                Thread.sleep(250);
            }
        }

        AssertionError timeout = new AssertionError(
            "WebView selector did not appear: " + selector
        );

        if (lastFailure != null) {
            timeout.initCause(lastFailure);
        }

        throw timeout;
    }

    private static void waitForElementText(String id, String expected)
        throws InterruptedException {
        long deadline =
            System.nanoTime() + TimeUnit.SECONDS.toNanos(PAGE_TIMEOUT_SECONDS);
        Throwable lastFailure = null;

        while (System.nanoTime() < deadline) {
            try {
                onWebView()
                    .withElement(findElement(Locator.ID, id))
                    .check(webMatches(getText(), containsString(expected)));
                return;
            } catch (AssertionError | RuntimeException failure) {
                lastFailure = failure;
                Thread.sleep(250);
            }
        }

        AssertionError timeout = new AssertionError(
            "WebView element #" + id + " did not contain: " + expected
        );

        if (lastFailure != null) {
            timeout.initCause(lastFailure);
        }

        throw timeout;
    }

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

        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());
    }
}
