/**
 * Action Tests: Walking
 *
 * Tests the core walk actions (startWalk, visitStop, completeTrail) and
 * automatically verifies eventual consistency between optimistic writes
 * and ingester behavior.
 *
 * These tests use a special setup (actions-setup.ts) that:
 * 1. Allows actions to run (not mocked out)
 * 2. Captures PDS operations scheduled via after()
 * 3. Verifies optimistic writes match what ingester would produce
 */

import { describe, it, expect } from "vitest";
import { emit, ALICE, BOB, setCurrentUser, generateTid, captureInitialState } from "./helpers";
import {
  startWalk,
  visitStop,
  completeTrail,
  abandonWalk,
  deleteCompletion,
  forgetTrail,
  publishDraft,
  deleteTrail,
} from "../actions";
import * as queries from "../queries";

const now = () => new Date().toISOString();

describe("Action: startWalk", () => {
  it("creates walk record with first stop visited", async () => {
    const stop1 = generateTid();
    const stop2 = generateTid();

    // Setup: create trail via ingester
    const trail = await emit("app.sidetrail.trail", ALICE.did, {
      $type: "app.sidetrail.trail",
      title: "Test Trail",
      description: "For testing startWalk",
      stops: [
        { tid: stop1, title: "First Stop", content: "Content 1" },
        { tid: stop2, title: "Second Stop", content: "Content 2" },
      ],
      accentColor: "#ff0000",
      backgroundColor: "#ffffff",
      createdAt: now(),
    });

    // Capture state before action
    await captureInitialState();

    // Act: Bob starts walking
    setCurrentUser(BOB);
    await startWalk(trail.uri, trail.cid);

    // Assert: Bob's walks list shows the trail
    const walks = await queries.loadWalks();
    expect(walks).toHaveLength(1);
    expect(walks[0].title).toBe("Test Trail");
    expect(walks[0].visitedStops).toEqual([stop1]);

    // Eventual consistency is automatically verified in afterEach
  });

  it("fails if trail has no stops", async () => {
    const trail = await emit("app.sidetrail.trail", ALICE.did, {
      $type: "app.sidetrail.trail",
      title: "Empty Trail",
      description: "No stops",
      stops: [],
      accentColor: "#ff0000",
      backgroundColor: "#ffffff",
      createdAt: now(),
    });

    await captureInitialState();
    setCurrentUser(BOB);

    await expect(startWalk(trail.uri, trail.cid)).rejects.toThrow("Trail has no stops");
  });
});

describe("Action: visitStop", () => {
  it("adds stop to visited list", async () => {
    const stop1 = generateTid();
    const stop2 = generateTid();
    const stop3 = generateTid();

    const trail = await emit("app.sidetrail.trail", ALICE.did, {
      $type: "app.sidetrail.trail",
      title: "Multi-stop Trail",
      description: "Progress through stops",
      stops: [
        { tid: stop1, title: "First", content: "1" },
        { tid: stop2, title: "Second", content: "2" },
        { tid: stop3, title: "Third", content: "3" },
      ],
      accentColor: "#00ff00",
      backgroundColor: "#ffffff",
      createdAt: now(),
    });

    // Bob starts walking (creates walk with stop1 visited)
    const walk = await emit("app.sidetrail.walk", BOB.did, {
      $type: "app.sidetrail.walk",
      trail: { uri: trail.uri, cid: trail.cid },
      visitedStops: [stop1],
      createdAt: now(),
      updatedAt: now(),
    });

    await captureInitialState();
    setCurrentUser(BOB);

    // Act: visit stop2
    await visitStop(walk.uri, stop2);

    // Assert: both stops are now visited
    const walks = await queries.loadWalks();
    expect(walks[0].visitedStops).toEqual([stop1, stop2]);

    // Eventual consistency is automatically verified in afterEach
  });

  it("revisiting a stop moves it to the end of visitedStops", async () => {
    const stop1 = generateTid();
    const stop2 = generateTid();
    const stop3 = generateTid();

    const trail = await emit("app.sidetrail.trail", ALICE.did, {
      $type: "app.sidetrail.trail",
      title: "Multi-stop Trail",
      description: "Test going back",
      stops: [
        { tid: stop1, title: "First", content: "1" },
        { tid: stop2, title: "Second", content: "2" },
        { tid: stop3, title: "Third", content: "3" },
      ],
      accentColor: "#0000ff",
      backgroundColor: "#ffffff",
      createdAt: now(),
    });

    // Bob has visited all 3 stops
    const walk = await emit("app.sidetrail.walk", BOB.did, {
      $type: "app.sidetrail.walk",
      trail: { uri: trail.uri, cid: trail.cid },
      visitedStops: [stop1, stop2, stop3],
      createdAt: now(),
      updatedAt: now(),
    });

    await captureInitialState();
    setCurrentUser(BOB);

    // Go back to stop2 - should move it to the end
    await visitStop(walk.uri, stop2);

    const walks = await queries.loadWalks();
    expect(walks[0].visitedStops).toEqual([stop1, stop3, stop2]);

    // Eventual consistency is automatically verified in afterEach
  });
});

describe("Action: completeTrail", () => {
  it("creates completion and removes walk", async () => {
    const stop1 = generateTid();

    const trail = await emit("app.sidetrail.trail", ALICE.did, {
      $type: "app.sidetrail.trail",
      title: "Completable Trail",
      description: "Can be finished",
      stops: [{ tid: stop1, title: "Final Stop", content: "Done!" }],
      accentColor: "#ffd700",
      backgroundColor: "#ffffff",
      createdAt: now(),
    });

    const walk = await emit("app.sidetrail.walk", BOB.did, {
      $type: "app.sidetrail.walk",
      trail: { uri: trail.uri, cid: trail.cid },
      visitedStops: [stop1],
      createdAt: now(),
      updatedAt: now(),
    });

    await captureInitialState();
    setCurrentUser(BOB);

    // Act: complete the trail
    await completeTrail(walk.uri);

    // Assert: walk is gone, completion exists
    const walks = await queries.loadWalks();
    expect(walks).toHaveLength(0);

    const completedTrails = await queries.loadUserCompletedTrails(BOB.handle);
    expect(completedTrails).toHaveLength(1);
    expect(completedTrails[0].title).toBe("Completable Trail");

    // Eventual consistency is automatically verified in afterEach
  });
});

describe("Action: abandonWalk", () => {
  it("removes walk from user's list", async () => {
    const stop1 = generateTid();

    const trail = await emit("app.sidetrail.trail", ALICE.did, {
      $type: "app.sidetrail.trail",
      title: "Abandonable Trail",
      description: "User might give up",
      stops: [{ tid: stop1, title: "Stop", content: "Content" }],
      accentColor: "#ff6b6b",
      backgroundColor: "#ffffff",
      createdAt: now(),
    });

    const walk = await emit("app.sidetrail.walk", BOB.did, {
      $type: "app.sidetrail.walk",
      trail: { uri: trail.uri, cid: trail.cid },
      visitedStops: [stop1],
      createdAt: now(),
      updatedAt: now(),
    });

    await captureInitialState();
    setCurrentUser(BOB);

    // Verify walk exists
    let walks = await queries.loadWalks();
    expect(walks).toHaveLength(1);

    // Act: abandon
    await abandonWalk(walk.uri);

    // Assert: walk is gone
    walks = await queries.loadWalks();
    expect(walks).toHaveLength(0);

    // Eventual consistency is automatically verified in afterEach
  });
});

describe("Action: deleteCompletion", () => {
  it("removes completion from user's list", async () => {
    const stop1 = generateTid();

    const trail = await emit("app.sidetrail.trail", ALICE.did, {
      $type: "app.sidetrail.trail",
      title: "Completed Trail",
      description: "Already finished",
      stops: [{ tid: stop1, title: "Stop", content: "Content" }],
      accentColor: "#00ff00",
      backgroundColor: "#ffffff",
      createdAt: now(),
    });

    const completion = await emit("app.sidetrail.completion", BOB.did, {
      $type: "app.sidetrail.completion",
      trail: { uri: trail.uri, cid: trail.cid },
      createdAt: now(),
    });

    await captureInitialState();
    setCurrentUser(BOB);

    // Verify completion exists
    let completions = await queries.loadUserCompletedTrails(BOB.handle);
    expect(completions).toHaveLength(1);

    // Act: delete completion
    await deleteCompletion(completion.uri);

    // Assert: completion is gone
    completions = await queries.loadUserCompletedTrails(BOB.handle);
    expect(completions).toHaveLength(0);

    // Eventual consistency is automatically verified in afterEach
  });
});

describe("Action: forgetTrail", () => {
  it("removes all walks and completions for a trail", async () => {
    const stop1 = generateTid();

    const trail = await emit("app.sidetrail.trail", ALICE.did, {
      $type: "app.sidetrail.trail",
      title: "Forgettable Trail",
      description: "User wants to forget",
      stops: [{ tid: stop1, title: "Stop", content: "Content" }],
      accentColor: "#purple",
      backgroundColor: "#ffffff",
      createdAt: now(),
    });

    // Bob has a walk on this trail
    await emit("app.sidetrail.walk", BOB.did, {
      $type: "app.sidetrail.walk",
      trail: { uri: trail.uri, cid: trail.cid },
      visitedStops: [stop1],
      createdAt: now(),
      updatedAt: now(),
    });

    // Bob also completed this trail before
    await emit("app.sidetrail.completion", BOB.did, {
      $type: "app.sidetrail.completion",
      trail: { uri: trail.uri, cid: trail.cid },
      createdAt: now(),
    });

    await captureInitialState();
    setCurrentUser(BOB);

    // Verify data exists
    let walks = await queries.loadWalks();
    let completions = await queries.loadUserCompletedTrails(BOB.handle);
    expect(walks).toHaveLength(1);
    expect(completions).toHaveLength(1);

    // Act: forget the trail
    await forgetTrail(trail.uri);

    // Assert: all data for this trail is gone
    walks = await queries.loadWalks();
    completions = await queries.loadUserCompletedTrails(BOB.handle);
    expect(walks).toHaveLength(0);
    expect(completions).toHaveLength(0);

    // Eventual consistency is automatically verified in afterEach
  });
});

describe("Action: publishDraft", () => {
  it("creates a new trail from draft data", async () => {
    await captureInitialState();
    setCurrentUser(BOB);

    const stop1Tid = generateTid();
    const stop2Tid = generateTid();

    // Act: publish a draft
    const result = await publishDraft({
      title: "My New Trail",
      description: "A trail I created",
      stops: [
        { tid: stop1Tid, title: "First Stop", content: "Welcome!" },
        { tid: stop2Tid, title: "Second Stop", content: "Keep going!" },
      ],
      accentColor: "#ff0000",
      backgroundColor: "#ffffff",
    });

    // Assert: trail was created
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.uri).toContain("app.sidetrail.trail");
      expect(result.handle).toBe(BOB.handle);
    }

    // Eventual consistency is automatically verified in afterEach
  });

  it("validates draft data", async () => {
    await captureInitialState();
    setCurrentUser(BOB);

    // Act: try to publish invalid draft
    const result = await publishDraft({
      title: "",
      description: "",
      stops: [{ tid: generateTid(), title: "Only One", content: "Need two" }],
      accentColor: "#ff0000",
      backgroundColor: "#ffffff",
    });

    // Assert: validation failed
    expect(result.success).toBe(false);
    if (!result.success) {
      expect(result.errors).toContain("trail needs a title");
      expect(result.errors).toContain("trail needs a description");
      expect(result.errors).toContain("trails need at least 2 stops to be published");
    }

    // Eventual consistency is automatically verified in afterEach
  });
});

describe("Action: deleteTrail", () => {
  it("removes trail from database", async () => {
    const stop1 = generateTid();

    // Bob creates a trail
    const trail = await emit("app.sidetrail.trail", BOB.did, {
      $type: "app.sidetrail.trail",
      title: "Deletable Trail",
      description: "Will be deleted",
      stops: [{ tid: stop1, title: "Stop", content: "Content" }],
      accentColor: "#ff0000",
      backgroundColor: "#ffffff",
      createdAt: now(),
    });

    await captureInitialState();
    setCurrentUser(BOB);

    // Act: delete the trail
    await deleteTrail(trail.uri);

    // Assert: trail is gone
    await expect(queries.loadTrailDetail(BOB.handle, trail.rkey)).rejects.toThrow(
      "Trail not found",
    );

    // Eventual consistency is automatically verified in afterEach
  });
});

/**
 * "Walked Here" List (Trail Walkers)
 *
 * Tests that the walkers list at the bottom of trail overview correctly
 * includes both walks AND completions, ensuring users don't disappear
 * after completing/abandoning a second attempt.
 */
describe("Walked here list (walkers)", () => {
  it("includes user with only a completion (no walk)", async () => {
    const stop1 = generateTid();

    const trail = await emit("app.sidetrail.trail", ALICE.did, {
      $type: "app.sidetrail.trail",
      title: "Completed Trail",
      description: "Bob completed this",
      stops: [{ tid: stop1, title: "Stop", content: "Content" }],
      accentColor: "#00ff00",
      backgroundColor: "#ffffff",
      createdAt: now(),
    });

    // Bob has a completion but NO walk (walked and completed previously)
    await emit("app.sidetrail.completion", BOB.did, {
      $type: "app.sidetrail.completion",
      trail: { uri: trail.uri, cid: trail.cid },
      createdAt: now(),
    });

    await captureInitialState();
    setCurrentUser(ALICE);

    // Assert: Bob should appear in walkers list
    const detail = await queries.loadTrailDetail(ALICE.handle, trail.rkey);
    const walkers = await detail.walkers;

    expect(walkers.map((w) => w.user.handle)).toContain(BOB.handle);
  });

  it("shows walk when user has both walk and completion", async () => {
    const stop1 = generateTid();

    const trail = await emit("app.sidetrail.trail", ALICE.did, {
      $type: "app.sidetrail.trail",
      title: "Trail with Both",
      description: "Bob has walk and completion",
      stops: [{ tid: stop1, title: "Stop", content: "Content" }],
      accentColor: "#0000ff",
      backgroundColor: "#ffffff",
      createdAt: now(),
    });

    // Bob completed the trail a while ago
    await emit("app.sidetrail.completion", BOB.did, {
      $type: "app.sidetrail.completion",
      trail: { uri: trail.uri, cid: trail.cid },
      createdAt: "2024-01-01T10:00:00Z",
    });

    // Bob started walking again (more recent)
    await emit("app.sidetrail.walk", BOB.did, {
      $type: "app.sidetrail.walk",
      trail: { uri: trail.uri, cid: trail.cid },
      visitedStops: [stop1],
      createdAt: "2024-06-01T10:00:00Z",
      updatedAt: "2024-06-01T10:00:00Z",
    });

    await captureInitialState();
    setCurrentUser(ALICE);

    // Assert: Bob should appear exactly once in walkers list
    const detail = await queries.loadTrailDetail(ALICE.handle, trail.rkey);
    const walkers = await detail.walkers;

    const bobEntries = walkers.filter((w) => w.user.handle === BOB.handle);
    expect(bobEntries).toHaveLength(1);
  });

  it("preserves user in walkers after abandoning second walk attempt (has completion)", async () => {
    const stop1 = generateTid();

    const trail = await emit("app.sidetrail.trail", ALICE.did, {
      $type: "app.sidetrail.trail",
      title: "Second Attempt Trail",
      description: "Bob will abandon second attempt",
      stops: [{ tid: stop1, title: "Stop", content: "Content" }],
      accentColor: "#ff6b6b",
      backgroundColor: "#ffffff",
      createdAt: now(),
    });

    // Bob completed the trail previously
    await emit("app.sidetrail.completion", BOB.did, {
      $type: "app.sidetrail.completion",
      trail: { uri: trail.uri, cid: trail.cid },
      createdAt: "2024-01-01T10:00:00Z",
    });

    // Bob started a second walk attempt
    const walk = await emit("app.sidetrail.walk", BOB.did, {
      $type: "app.sidetrail.walk",
      trail: { uri: trail.uri, cid: trail.cid },
      visitedStops: [stop1],
      createdAt: "2024-06-01T10:00:00Z",
      updatedAt: "2024-06-01T10:00:00Z",
    });

    await captureInitialState();
    setCurrentUser(BOB);

    // Bob abandons the second walk
    await abandonWalk(walk.uri);

    // Assert: Bob should STILL appear in walkers list (via completion)
    const detail = await queries.loadTrailDetail(ALICE.handle, trail.rkey);
    const walkers = await detail.walkers;

    expect(walkers.map((w) => w.user.handle)).toContain(BOB.handle);
  });

  it("preserves user in walkers after completing second walk attempt", async () => {
    const stop1 = generateTid();

    const trail = await emit("app.sidetrail.trail", ALICE.did, {
      $type: "app.sidetrail.trail",
      title: "Re-complete Trail",
      description: "Bob will complete again",
      stops: [{ tid: stop1, title: "Stop", content: "Content" }],
      accentColor: "#ffd700",
      backgroundColor: "#ffffff",
      createdAt: now(),
    });

    // Bob completed the trail previously
    await emit("app.sidetrail.completion", BOB.did, {
      $type: "app.sidetrail.completion",
      trail: { uri: trail.uri, cid: trail.cid },
      createdAt: "2024-01-01T10:00:00Z",
    });

    // Bob started a second walk attempt
    const walk = await emit("app.sidetrail.walk", BOB.did, {
      $type: "app.sidetrail.walk",
      trail: { uri: trail.uri, cid: trail.cid },
      visitedStops: [stop1],
      createdAt: "2024-06-01T10:00:00Z",
      updatedAt: "2024-06-01T10:00:00Z",
    });

    await captureInitialState();
    setCurrentUser(BOB);

    // Bob completes the trail again
    await completeTrail(walk.uri);

    // Assert: Bob should still appear (now with 2 completions)
    const detail = await queries.loadTrailDetail(ALICE.handle, trail.rkey);
    const walkers = await detail.walkers;

    expect(walkers.map((w) => w.user.handle)).toContain(BOB.handle);
  });

  it("user disappears only after forgetTrail (removes both walks and completions)", async () => {
    const stop1 = generateTid();

    const trail = await emit("app.sidetrail.trail", ALICE.did, {
      $type: "app.sidetrail.trail",
      title: "Forgettable Trail",
      description: "Bob will forget everything",
      stops: [{ tid: stop1, title: "Stop", content: "Content" }],
      accentColor: "#purple",
      backgroundColor: "#ffffff",
      createdAt: now(),
    });

    // Bob has a completion
    await emit("app.sidetrail.completion", BOB.did, {
      $type: "app.sidetrail.completion",
      trail: { uri: trail.uri, cid: trail.cid },
      createdAt: now(),
    });

    await captureInitialState();
    setCurrentUser(BOB);

    // Verify Bob is in walkers
    let detail = await queries.loadTrailDetail(ALICE.handle, trail.rkey);
    let walkers = await detail.walkers;
    expect(walkers.map((w) => w.user.handle)).toContain(BOB.handle);

    // Bob forgets the trail entirely
    await forgetTrail(trail.uri);

    // Assert: Bob should no longer appear
    detail = await queries.loadTrailDetail(ALICE.handle, trail.rkey);
    walkers = await detail.walkers;
    expect(walkers.map((w) => w.user.handle)).not.toContain(BOB.handle);
  });
});

/**
 * Duplicate Walk Handling (Regression Tests)
 *
 * These tests verify that actions correctly handle duplicate walks
 * that may exist due to ATProto sync issues, race conditions, etc.
 * Our interpretation is "one walk per trail per user".
 */
describe("Duplicate walk handling in actions", () => {
  it("startWalk does not create duplicate if walk already exists", async () => {
    const stop1 = generateTid();

    const trail = await emit("app.sidetrail.trail", ALICE.did, {
      $type: "app.sidetrail.trail",
      title: "No Duplicate Test",
      description: "Testing startWalk idempotency",
      stops: [{ tid: stop1, title: "Stop", content: "Content" }],
      accentColor: "#ff0000",
      backgroundColor: "#ffffff",
      createdAt: now(),
    });

    // Bob already has a walk (created via ingester)
    await emit("app.sidetrail.walk", BOB.did, {
      $type: "app.sidetrail.walk",
      trail: { uri: trail.uri, cid: trail.cid },
      visitedStops: [stop1],
      createdAt: now(),
      updatedAt: now(),
    });

    await captureInitialState();
    setCurrentUser(BOB);

    // Verify walk exists
    let walks = await queries.loadWalks();
    expect(walks).toHaveLength(1);

    // Act: try to start walk again
    await startWalk(trail.uri, trail.cid);

    // Assert: still only one walk (no duplicate created)
    walks = await queries.loadWalks();
    expect(walks).toHaveLength(1);
  });

  it("completeTrail deletes ALL duplicate walks", async () => {
    const stop1 = generateTid();

    const trail = await emit("app.sidetrail.trail", ALICE.did, {
      $type: "app.sidetrail.trail",
      title: "Complete With Duplicates",
      description: "Testing complete cleans up duplicates",
      stops: [{ tid: stop1, title: "Stop", content: "Content" }],
      accentColor: "#00ff00",
      backgroundColor: "#ffffff",
      createdAt: now(),
    });

    // Bob has THREE duplicate walks (simulating ATProto sync issues)
    await emit("app.sidetrail.walk", BOB.did, {
      $type: "app.sidetrail.walk",
      trail: { uri: trail.uri, cid: trail.cid },
      visitedStops: [],
      createdAt: "2024-01-01T10:00:00Z",
      updatedAt: "2024-01-01T10:00:00Z",
    });

    await emit("app.sidetrail.walk", BOB.did, {
      $type: "app.sidetrail.walk",
      trail: { uri: trail.uri, cid: trail.cid },
      visitedStops: [stop1],
      createdAt: "2024-03-01T10:00:00Z",
      updatedAt: "2024-03-01T10:00:00Z",
    });

    const walk3 = await emit("app.sidetrail.walk", BOB.did, {
      $type: "app.sidetrail.walk",
      trail: { uri: trail.uri, cid: trail.cid },
      visitedStops: [stop1],
      createdAt: "2024-06-01T10:00:00Z",
      updatedAt: "2024-06-01T10:00:00Z",
    });

    await captureInitialState();
    setCurrentUser(BOB);

    // loadWalks shows 1 (deduped), but there are 3 in DB
    const walks = await queries.loadWalks();
    expect(walks).toHaveLength(1);

    // Act: complete the trail (using most recent walk)
    await completeTrail(walk3.uri);

    // Assert: ALL walks are gone, completion exists
    const walksAfter = await queries.loadWalks();
    expect(walksAfter).toHaveLength(0);

    const completions = await queries.loadUserCompletedTrails(BOB.handle);
    expect(completions).toHaveLength(1);

    // Trail detail should show no walkers (all duplicates cleaned up)
    const detail = await queries.loadTrailDetail(ALICE.handle, trail.rkey);
    const walkers = await detail.walkers;
    // Only the completion should show, not any walks
    const walkingWalkers = walkers.filter((w) => !w.isYou);
    expect(walkingWalkers.every((w) => w.user.handle !== BOB.handle)).toBe(true);
  });

  it("abandonWalk deletes ALL duplicate walks", async () => {
    const stop1 = generateTid();

    const trail = await emit("app.sidetrail.trail", ALICE.did, {
      $type: "app.sidetrail.trail",
      title: "Abandon With Duplicates",
      description: "Testing abandon cleans up duplicates",
      stops: [{ tid: stop1, title: "Stop", content: "Content" }],
      accentColor: "#0000ff",
      backgroundColor: "#ffffff",
      createdAt: now(),
    });

    // Bob has TWO duplicate walks
    await emit("app.sidetrail.walk", BOB.did, {
      $type: "app.sidetrail.walk",
      trail: { uri: trail.uri, cid: trail.cid },
      visitedStops: [],
      createdAt: "2024-01-01T10:00:00Z",
      updatedAt: "2024-01-01T10:00:00Z",
    });

    const walk2 = await emit("app.sidetrail.walk", BOB.did, {
      $type: "app.sidetrail.walk",
      trail: { uri: trail.uri, cid: trail.cid },
      visitedStops: [stop1],
      createdAt: "2024-06-01T10:00:00Z",
      updatedAt: "2024-06-01T10:00:00Z",
    });

    await captureInitialState();
    setCurrentUser(BOB);

    // Verify walks exist (shows 1 due to dedup)
    let walks = await queries.loadWalks();
    expect(walks).toHaveLength(1);

    // Act: abandon the walk (using most recent)
    await abandonWalk(walk2.uri);

    // Assert: ALL walks are gone
    walks = await queries.loadWalks();
    expect(walks).toHaveLength(0);

    // Trail should show no Bob
    const detail = await queries.loadTrailDetail(ALICE.handle, trail.rkey);
    const walkers = await detail.walkers;
    expect(walkers.map((w) => w.user.handle)).not.toContain(BOB.handle);
  });
});
