import { http, HttpResponse } from "msw";
import preview from "../../.storybook/preview.tsx";
import { sleep } from "../sleep.ts";
import type { CurrentUser, SessionInfo } from "../types.ts";
import { CurrentUserFetcher } from "./CurrentUserFetcher.tsx";

function createMocks(options?: { responseSpeed?: number }) {
  const simulateNetwork = () => sleep(options?.responseSpeed ?? 2000);

  const john: CurrentUser = {
    id: "john",
    email: "john@example.com",
    isVerified: true,
  };

  const mary: CurrentUser = {
    id: "mary",
    email: "mary@example.com",
    isVerified: true,
  };

  const vernon: CurrentUser = {
    id: "vernon",
    email: "vernon@example.com",
    isVerified: false,
  };

  const sessionInfo: SessionInfo = {
    createdTs: 123,
    expiresTs: 123,
  };

  return {
    data: { john, mary, vernon },
    handlers: {
      refreshTokens: {
        success: () => {
          return http.post(
            "http://mock.api/v1/sessions/access-tokens",
            async () => {
              await simulateNetwork();
              return HttpResponse.json({ sessionInfo });
            },
          );
        },

        failed: () => {
          return http.post(
            "http://mock.api/v1/sessions/access-tokens",
            async () => {
              await simulateNetwork();
              return HttpResponse.text("Refresh token expired or missing", {
                status: 401,
              });
            },
          );
        },
      },

      getCurrentUser: {
        success: (currentUser: CurrentUser) => {
          return http.get("http://mock.api/v1/users/me", async () => {
            await simulateNetwork();
            return HttpResponse.json(currentUser);
          });
        },

        /**
         * Cookie is valid, but user doesn't exist any more. This can happen
         * after user deletion.
         */
        notFound: () => {
          return http.get("http://mock.api/v1/users/me", async () => {
            await simulateNetwork();
            return HttpResponse.text("User not found", { status: 404 });
          });
        },

        noConnection: () => {
          return http.get("http://mock.api/v1/users/me", async () => {
            return HttpResponse.error();
          });
        },

        unknownFailure: () => {
          return http.get("http://mock.api/v1/users/me", async () => {
            await simulateNetwork();
            return HttpResponse.text("Internal server error", { status: 500 });
          });
        },

        /**
         * Auth cookies no longer valid to make this request.
         */
        notAuthenticated: () => {
          return http.get("http://mock.api/v1/users/me", async () => {
            await simulateNetwork();
            return HttpResponse.text("Not authenticated", { status: 401 });
          });
        },
      },

      createNewSession: {
        success: (currentUser: CurrentUser) => {
          return http.post("http://mock.api/v1/sessions", async () => {
            await simulateNetwork();
            return HttpResponse.json({ currentUser, sessionInfo });
          });
        },

        invalidCredentials: () => {
          return http.post("http://mock.api/v1/sessions", async () => {
            await simulateNetwork();
            return HttpResponse.text("Credentials do not match", {
              status: 401,
            });
          });
        },

        userNotFound: () => {
          return http.post("http://mock.api/v1/sessions", async () => {
            await simulateNetwork();
            return HttpResponse.text("User not found", { status: 404 });
          });
        },

        unknownFailure: () => {
          return http.post("http://mock.api/v1/sessions", async () => {
            await simulateNetwork();
            return HttpResponse.text("Internal server error", { status: 500 });
          });
        },
      },

      requestVerify: {
        success: () => {
          return http.post(
            "http://mock.api/v1/user-verification-tokens",
            async () => {
              await simulateNetwork();
              return HttpResponse.json({ message: "OK", tokenId: "1" });
            },
          );
        },
      },

      verify: {
        success: () => {
          return http.put(
            "http://mock.api/v1/user-verification-tokens/:id",
            async () => {
              await simulateNetwork();
              return HttpResponse.json({ message: "OK" });
            },
          );
        },
      },
    },
  };
}

const { data, handlers } = createMocks({ responseSpeed: 700 });

const meta = preview.meta({
  title: "Account/Current User Fetcher",
  component: CurrentUserFetcher,
  tags: ["autodocs"],
  args: {
    children: (
      <div
        style={{
          minBlockSize: "20rem",
          backgroundColor: "white",
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          borderRadius: "1rem",
        }}
      >
        App content
      </div>
    ),
  },
  parameters: {
    msw: {
      handlers: {
        refreshTokens: handlers.refreshTokens.failed(),
      },
    },
  },
});

/**
 * The default case, in which a local user is provided (so they have previously
 * logged in), and the current user request returns the same user as is
 * the local user (determined by the user id).
 */
export const Default = meta.story({
  parameters: {
    msw: {
      handlers: {
        getCurrentUser: handlers.getCurrentUser.success(data.john),
      },
    },
  },
});

/**
 * In this case, no local user is provided and the component simply renders
 * its children. There should be no network request in this case.
 */
export const NoLocalUser = meta.story({});

/**
 * In this case, the local user is provided and the current user request returns
 * error 401. The user should supply their credentials again.
 */
export const SessionExpired = meta.story({
  parameters: {
    msw: {
      handlers: {
        getCurrentUser: handlers.getCurrentUser.notAuthenticated(),
      },
    },
  },
});

/**
 * In this case, the local user is provided and the current user request returns
 * a different user (determined by user ID).
 *
 * This case can happen when user A logs into app X, then user B logs into
 * app Y. Returning to app X, user B will be logged into ITC with their account
 * but local data belong to user A.
 *
 * In practice this should be a rare case, but with unpleasant circumstances
 * if not handled correctly.
 */
export const UserMismatch = meta.story({
  parameters: {
    msw: {
      handlers: {
        getCurrentUser: handlers.getCurrentUser.success(data.mary),
      },
    },
  },
});

/**
 */
export const UserUnverified = meta.story({
  parameters: {
    msw: {
      handlers: {
        getCurrentUser: handlers.getCurrentUser.success(data.vernon),
        requestVerify: handlers.requestVerify.success(),
        verify: handlers.verify.success(),
        refreshTokens: handlers.refreshTokens.success(),
      },
    },
  },
});

/**
 * In this case, the local user is provided and the current user request returns
 * 404, indicating that the current user no longer exists.
 *
 * This can happen if users delete their accounts but
 */
export const UserNotFound = meta.story({
  parameters: {
    msw: {
      handlers: {
        getCurrentUser: handlers.getCurrentUser.notFound(),
      },
    },
  },
});

/**
 * The proactive user session check has failed due to an error that doesn't
 * carry any special meaning.
 */
export const UnknownFailure = meta.story({
  parameters: {
    msw: {
      handlers: {
        getCurrentUser: handlers.getCurrentUser.unknownFailure(),
      },
    },
  },
});
