import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
  createJourney,
  decodeJourneySnapshot,
  encodeJourneySnapshot,
  endpointCity,
  clearJourneySession,
  journeyReducer,
  loadJourneySession,
  officeCity,
  saveJourneySession,
  transitionJourney,
  type JourneyActor,
  type JourneyEvent,
  type JourneyState,
  type JourneyStorageAdapter,
} from "../journey/index.ts";

const CROW: JourneyActor = {
  id: "anon-115",
  kind: "crow",
  signedIn: false,
  profile: { displayName: "Visitor", color: "midnight" },
};

const KARTI: JourneyActor = {
  id: "user-karti",
  kind: "humanoid",
  signedIn: true,
  profile: { displayName: "Karti", face: "profile:user-karti", color: "black" },
};

function playFromLosAngeles(): JourneyState {
  let state = createJourney({ actor: CROW, location: { scale: "california" }, mode: "play" });
  state = journeyReducer(state, {
    type: "select-route",
    routeId: "la-sf-i-5",
    direction: 1,
  });
  state = journeyReducer(state, { type: "enter-vehicle", vehicleId: "model-x-hero" });
  return state;
}

describe("journey state machine", () => {
  it("treats metro boards as geography, not as a journey scale", () => {
    const california = createJourney({ actor: CROW, mode: "play" });
    assert.deepEqual(
      transitionJourney(california, { type: "navigate-to-city", city: "bay-area" }),
      { accepted: false, state: california, reason: "already-there" },
    );
    assert.deepEqual(
      transitionJourney(california, { type: "return-to-california" }),
      { accepted: false, state: california, reason: "already-there" },
    );
    assert.deepEqual(california.location, { scale: "california" });
    assert.equal(endpointCity("los-angeles"), "socal");
    assert.equal(endpointCity("san-francisco"), "bay-area");
    assert.equal(officeCity("mateo-court"), "socal");
  });

  it("requires leaving an office or vehicle before the leftover board events", () => {
    let driving = playFromLosAngeles();
    assert.deepEqual(
      transitionJourney(driving, { type: "navigate-to-city", city: "bay-area" }),
      { accepted: false, state: driving, reason: "exit-vehicle-first" },
    );

    driving = journeyReducer(driving, { type: "exit-vehicle" });
    let office = journeyReducer(driving, {
      type: "enter-office",
      officeId: "lumbridge-hq",
    });
    assert.deepEqual(
      transitionJourney(office, { type: "return-to-california" }),
      { accepted: false, state: office, reason: "leave-office-first" },
    );
    office = journeyReducer(office, { type: "leave-office" });
    assert.deepEqual(office.location, { scale: "california" });
  });

  it("arrives at a route end still on California", () => {
    let northbound = playFromLosAngeles();
    northbound = journeyReducer(northbound, { type: "update-route-progress", progress: 0.74 });
    northbound = journeyReducer(northbound, {
      type: "reach-route-endpoint",
      endpoint: "san-francisco",
    });
    assert.equal(northbound.location.scale, "california");
    assert.equal(northbound.route?.progress, 1);
    assert.equal(northbound.vehicle?.vehicleId, "model-x-hero");

    let southbound = journeyReducer(northbound, { type: "exit-vehicle" });
    southbound = journeyReducer(southbound, {
      type: "select-route",
      routeId: "la-sf-us-101",
      direction: -1,
    });
    southbound = journeyReducer(southbound, { type: "enter-vehicle", vehicleId: "model-x-hero" });
    southbound = journeyReducer(southbound, {
      type: "reach-route-endpoint",
      endpoint: "los-angeles",
    });
    assert.equal(southbound.location.scale, "california");
    assert.equal(southbound.route?.progress, 0);
  });

  it("opens an office from California and leaves onto the same world", () => {
    let state = createJourney({ actor: KARTI, location: { scale: "california" }, mode: "play" });
    const actor = state.actor;
    state = journeyReducer(state, { type: "enter-office", officeId: "frontier-valley" });
    assert.deepEqual(state.location, {
      scale: "office",
      officeId: "frontier-valley",
    });
    assert.equal(state.actor, actor);
    state = journeyReducer(state, { type: "leave-office" });
    assert.deepEqual(state.location, { scale: "california" });
    assert.equal(state.actor, actor);
  });

  it("swaps a signed-in actor without disturbing their journey", () => {
    const before = playFromLosAngeles();
    const after = journeyReducer(before, { type: "sign-in-actor-swap", actor: KARTI });
    assert.deepEqual(after.actor, KARTI);
    assert.notEqual(after.actor, KARTI, "the reducer owns a defensive actor copy");
    assert.deepEqual(after.location, before.location);
    assert.deepEqual(after.vehicle, before.vehicle);
    assert.deepEqual(after.route, before.route);
  });

  it("rejects invalid and out-of-order transitions as exact no-ops", () => {
    const state = createJourney({ actor: CROW });
    const badEvents: JourneyEvent[] = [
      { type: "enter-vehicle", vehicleId: "model-x-hero" },
      { type: "leave-office" },
      { type: "update-route-progress", progress: 0.5 },
      { type: "reach-route-endpoint", endpoint: "san-francisco" },
      { type: "sign-in-actor-swap", actor: CROW },
    ];
    for (const event of badEvents) {
      const result = transitionJourney(state, event);
      assert.equal(result.accepted, false);
      assert.equal(result.state, state);
      assert.equal(journeyReducer(state, event), state);
    }
  });

  it("requires the endpoint that agrees with the selected direction", () => {
    const state = playFromLosAngeles();
    const rejected = transitionJourney(state, {
      type: "reach-route-endpoint",
      endpoint: "los-angeles",
    });
    assert.deepEqual(rejected, { accepted: false, state, reason: "wrong-endpoint" });
  });

  it("requires play mode, a route, and exiting the car before an office", () => {
    const observer = createJourney({ actor: CROW, location: { scale: "california" } });
    assert.equal(
      transitionJourney(observer, { type: "enter-vehicle", vehicleId: "x" }).accepted,
      false,
    );

    let driving = playFromLosAngeles();
    driving = journeyReducer(driving, {
      type: "reach-route-endpoint",
      endpoint: "san-francisco",
    });
    const result = transitionJourney(driving, {
      type: "enter-office",
      officeId: "lumbridge-hq",
    });
    assert.deepEqual(result, { accepted: false, state: driving, reason: "exit-vehicle-first" });
  });

  it("encodes and decodes an isolated versioned reconnect snapshot", () => {
    const state = playFromLosAngeles();
    const encoded = encodeJourneySnapshot(state);
    const decoded = decodeJourneySnapshot(encoded);
    assert.equal(decoded.ok, true);
    if (!decoded.ok) return;
    assert.equal(decoded.snapshot.version, 1);
    assert.deepEqual(decoded.state, state);
    assert.notEqual(decoded.state, state);
    decoded.state.actor.profile.displayName = "Changed offline";
    assert.equal(state.actor.profile.displayName, "Visitor");
  });

  it("fails closed on corrupt, inconsistent, and future snapshots", () => {
    assert.deepEqual(decodeJourneySnapshot("{"), {
      ok: false,
      error: "journey: snapshot is not valid JSON",
    });
    assert.equal(decodeJourneySnapshot({ version: 2, state: {} }).ok, false);
    assert.equal(
      decodeJourneySnapshot({
        version: 1,
        state: {
          ...createJourney({ actor: CROW }),
          location: {
            scale: "office",
            officeId: "not-an-office",
          },
        },
      }).ok,
      false,
    );
  });

  it("reads a three-board session as California", () => {
    const outdoor = decodeJourneySnapshot({
      version: 1,
      state: {
        ...createJourney({ actor: CROW, mode: "play" }),
        location: { scale: "bay-area" },
      },
    });
    assert.equal(outdoor.ok, true);
    if (outdoor.ok) assert.deepEqual(outdoor.state.location, { scale: "california" });

    const inside = decodeJourneySnapshot({
      version: 1,
      state: {
        ...createJourney({ actor: KARTI, mode: "play" }),
        location: {
          scale: "office",
          officeId: "lumbridge-hq",
          priorCity: "bay-area",
        },
      },
    });
    assert.equal(inside.ok, true);
    if (inside.ok) {
      assert.deepEqual(inside.state.location, {
        scale: "office",
        officeId: "lumbridge-hq",
      });
    }
  });

  it("is deterministic for the same initial state and event log", () => {
    const initial = createJourney({ actor: CROW, location: { scale: "california" }, mode: "play" });
    const events: JourneyEvent[] = [
      { type: "select-route", routeId: "la-sf-us-101", direction: 1 },
      { type: "enter-vehicle", vehicleId: "model-x-hero" },
      { type: "update-route-progress", progress: 0.25 },
      { type: "update-route-progress", progress: 0.75 },
      { type: "reach-route-endpoint", endpoint: "san-francisco" },
      { type: "exit-vehicle" },
      { type: "enter-office", officeId: "lumbridge-hq" },
      { type: "sign-in-actor-swap", actor: KARTI },
    ];
    const run = (): JourneyState => events.reduce(journeyReducer, initial);
    assert.deepEqual(run(), run());
  });
});

class MemoryStorage implements JourneyStorageAdapter {
  readonly values = new Map<string, string>();

  getItem(key: string): string | null {
    return this.values.get(key) ?? null;
  }

  setItem(key: string, value: string): void {
    this.values.set(key, value);
  }

  removeItem(key: string): void {
    this.values.delete(key);
  }
}

describe("journey session persistence", () => {
  it("round-trips through a caller-owned storage adapter and clears cleanly", () => {
    const storage = new MemoryStorage();
    const state = playFromLosAngeles();
    assert.deepEqual(saveJourneySession(storage, "tera.journey", state), { ok: true });
    const loaded = loadJourneySession(storage, "tera.journey");
    assert.equal(loaded.status, "loaded");
    if (loaded.status !== "loaded") return;
    assert.deepEqual(loaded.state, state);
    assert.notEqual(loaded.state, state);
    assert.deepEqual(clearJourneySession(storage, "tera.journey"), { ok: true });
    assert.deepEqual(loadJourneySession(storage, "tera.journey"), { status: "missing" });
  });

  it("rejects corrupt and future sessions without throwing or mutating storage", () => {
    const storage = new MemoryStorage();
    storage.values.set("corrupt", "{");
    storage.values.set("future", JSON.stringify({ version: 2, state: {} }));
    assert.equal(loadJourneySession(storage, "corrupt").status, "invalid");
    assert.deepEqual(loadJourneySession(storage, "future"), {
      status: "invalid",
      error: "journey: unsupported snapshot version",
    });
    assert.equal(storage.values.has("future"), true);
  });

  it("contains adapter exceptions and invalid keys", () => {
    const unavailable: JourneyStorageAdapter = {
      getItem: () => {
        throw new Error("blocked");
      },
      setItem: () => {
        throw new Error("full");
      },
      removeItem: () => {
        throw new Error("blocked");
      },
    };
    const state = playFromLosAngeles();
    assert.equal(loadJourneySession(unavailable, "tera.journey").status, "unavailable");
    assert.equal(saveJourneySession(unavailable, "tera.journey", state).ok, false);
    assert.equal(clearJourneySession(unavailable, "tera.journey").ok, false);
    assert.equal(loadJourneySession(unavailable, " ").status, "invalid");
  });
});
