import { afterEach, describe, expect, mock, test } from "bun:test";
import { testContext, type TestContext } from "@routecraft/testing";
import { craft, simple, type CraftPlugin } from "@routecraft/routecraft";

describe("CraftContext", () => {
  let t: TestContext;

  afterEach(async () => {
    if (t) {
      await t.stop();
    }
  });

  /**
   * @case Verifies context initialization with minimal configuration
   * @preconditions None
   * @expectedResult Context should start and stop without errors
   */
  test("Initializes with empty configuration", async () => {
    t = await testContext().build();
    const execution = t.ctx.start();

    await t.ctx.stop();
    await execution;

    expect(t.ctx.getRoutes()).toHaveLength(0);
  });

  /**
   * @case Validates route registration functionality
   * @preconditions Simple route definition exists
   * @expectedResult Context should contain exactly 1 registered route
   */
  test("Registers routes correctly", async () => {
    t = await testContext()
      .routes(craft().id("test-route").from(simple("test")))
      .build();

    await t.ctx.start();
    // Ensure all asynchronous logs have flushed
    await new Promise((r) => setTimeout(r, 0));

    expect(t.ctx.getRoutes()).toHaveLength(1);
    expect(t.ctx.getRouteById("test-route")).toBeDefined();
  });

  /**
   * @case Verifies RouteBuilder is accepted via brand (works across package copies via Symbol.for())
   * @preconditions RouteBuilder instance (e.g. from user module when CLI uses different copy)
   * @expectedResult Context should register the route and start without validation errors
   */
  test("Accepts RouteBuilder (brand allows cross-instance recognition)", async () => {
    const builder = craft().id("duck-route").from(simple("duck"));
    t = await testContext().routes(builder).build();

    await t.ctx.start();
    await new Promise((r) => setTimeout(r, 0));

    expect(t.ctx.getRoutes()).toHaveLength(1);
    expect(t.ctx.getRouteById("duck-route")).toBeDefined();
  });

  /**
   * @case Verifies lifecycle hooks execution
   * @preconditions Context with startup/shutdown handlers
   * @expectedResult Both handlers should be called exactly once
   */
  test("Executes lifecycle events", async () => {
    const contextStarting = mock();
    const contextStarted = mock();
    const contextStopping = mock();
    const contextStopped = mock();

    t = await testContext()
      .on("context:starting", contextStarting)
      .on("context:started", contextStarted)
      .on("context:stopping", contextStopping)
      .on("context:stopped", contextStopped)
      .build();

    await t.ctx.start();

    expect(contextStarting).toHaveBeenCalledTimes(1);
    expect(contextStarted).toHaveBeenCalledTimes(1);
    expect(contextStopping).toHaveBeenCalledTimes(1);
    expect(contextStopped).toHaveBeenCalledTimes(1);
  });
});

describe("Error Handling", () => {
  let t: TestContext;

  afterEach(async () => {
    if (t) {
      await t.stop();
    }
  });

  /**
   * @case Verifies error handling in startup sequence
   * @preconditions Context with failing startup handler
   * @expectedResult Context should throw error and shutdown
   */
  test("Handles startup errors", async () => {
    const errorMessage = "Simulated startup failure";
    t = await testContext()
      .on("context:starting", () => {
        throw new Error(errorMessage);
      })
      .build();

    // Start won't reject because we emit errors and continue; verify error event fires
    const errSpy = mock();
    t.ctx.on("context:error", errSpy);
    await t.ctx.start();
    await new Promise((r) => setTimeout(r, 0));
    expect(errSpy).toHaveBeenCalled();
  });
});

describe("Route Management", () => {
  let t: TestContext;

  afterEach(async () => {
    if (t) {
      await t.stop();
    }
  });

  /**
   * @case Validates duplicate route ID prevention
   * @preconditions Two routes with same ID
   * @expectedResult Should throw error during context creation
   */
  test("Rejects duplicate route IDs", async () => {
    const builder = testContext().routes(
      craft()
        .id("duplicate")
        .from(simple("test"))
        .id("duplicate")
        .from(simple("test")),
    );

    await expect(builder.build()).rejects.toThrow(/duplicate/i);
  });

  /**
   * @case Verifies route retrieval behavior
   * @preconditions Context with multiple routes
   * @expectedResult Correct route retrieval and undefined for missing
   */
  test("Manages multiple routes correctly", async () => {
    const testRoutes = [1, 2, 3]
      .map((n) => craft().id(`route-${n}`).from(simple(n)).build())
      .flat();

    t = await testContext().routes(testRoutes).build();

    expect(t.ctx.getRoutes()).toHaveLength(3);
    expect(t.ctx.getRouteById("route-2")).toBeDefined();
    expect(t.ctx.getRouteById("missing")).toBeUndefined();
  });
});

describe("Lifecycle Management", () => {
  let t: TestContext;

  afterEach(async () => {
    if (t) {
      await t.stop();
    }
  });

  /**
   * @case Verifies idempotent stop behavior
   * @preconditions Active context
   * @expectedResult Subsequent stop calls are no-ops
   */
  test("Handles multiple stop calls", async () => {
    t = await testContext().build();
    await t.ctx.start();

    await t.ctx.stop();
    // Second stop should be a no-op; assert it resolves without throwing.
    // Avoid `.resolves.not.toThrow()` -- bun:test rejects when the resolved
    // value is undefined (vitest accepts).
    await expect(t.ctx.stop()).resolves.toBeUndefined();
  });

  /**
   * @case Verifies context:stopped is emitted only after teardown work completes
   * @preconditions Context with async plugin teardown and registered teardown callback
   * @expectedResult Event ordering is plugin teardown, registered teardown, then context:stopped
   */
  test("Emits context:stopped after teardown completes", async () => {
    const lifecycle: string[] = [];

    const plugin: CraftPlugin = {
      apply(ctx) {
        ctx.registerTeardown(async () => {
          await new Promise((resolve) => setTimeout(resolve, 10));
          lifecycle.push("callback");
        });
      },
      async teardown() {
        await new Promise((resolve) => setTimeout(resolve, 10));
        lifecycle.push("plugin");
      },
    };

    t = await testContext()
      .with({ plugins: [plugin] })
      .build();

    t.ctx.on("context:stopped", () => {
      lifecycle.push("context:stopped");
    });

    await t.ctx.stop();

    expect(lifecycle).toEqual(["plugin", "callback", "context:stopped"]);
  });

  /**
   * @case Validates store initialization
   * @preconditions Context with custom store
   * @expectedResult Store should be available in context
   */
  test("Initializes custom stores", async () => {
    const testStore = new Map([["test", "value"]]);

    t = await testContext()
      .store("customStore" as any, testStore)
      .build();

    expect(t.ctx.getStore("customStore" as any)).toBe(testStore);
  });
});

describe("Route Independence", () => {
  let t: TestContext;

  afterEach(async () => {
    if (t) {
      await t.stop();
    }
  });

  /**
   * @case Verifies that failed routes don't prevent others from processing and calls destination adapter
   * @preconditions Context with failing and working routes
   * @expectedResult Working route should process and eventually call destination adapter
   */
  test("Failed route does not prevent others from processing and calls destination adapter", async () => {
    const sendSpy = mock();

    // Create context with failing and working routes.
    t = await testContext()
      .routes(
        craft()
          .id("failing-route")
          .from(
            simple(() => {
              throw new Error("Simulated route failure");
            }),
          )
          .id("failing-route2")
          .from(() => {
            throw new Error("Simulated route failure");
          })
          .process(() => {
            throw new Error("Simulated route failure");
          })
          .id("working-route")
          .from(simple("work"))
          .to(sendSpy),
      )
      .build();

    // Start the context. The working route should process and eventually
    // trigger the destination adapter's send() from the "to" step.
    await t.ctx.start();

    // Assert that the NoopAdapter's send method was called.
    expect(sendSpy).toHaveBeenCalled();
  });
});

// Binder-related tests removed
