import { describe, it, expect, beforeEach, mock, spyOn } from "bun:test";
import {
  createMockRuntime,
  createMockMemory,
  createMockState,
  mockLogger,
} from "./test-utils";
import { ChainType } from "@0xsquid/squid-types";
import { elizaLogger } from "@elizaos/core";

// Create mock functions
const mockInit = mock().mockResolvedValue(undefined);
const mockGetRoute = mock().mockResolvedValue({
  route: {
    estimate: { toAmount: "1000000000000000000" },
    transactionRequest: {
      target: "0x1234567890123456789012345678901234567890",
      data: "0x",
      value: "0",
    },
  },
});
const mockExecuteRoute = mock().mockResolvedValue({
  hash: "0x1234567890123456789012345678901234567890123456789012345678901234",
  wait: mock().mockResolvedValue({
    hash: "0x1234567890123456789012345678901234567890123456789012345678901234",
  }),
});

// Mock the providers module
const mockSquidProvider = {
  initialize: mockInit,
  getChain: mock().mockImplementation((chainName: string) => {
    const chains: Record<string, any> = {
      ethereum: {
        chainId: "1",
        networkName: "ethereum",
        chainType: ChainType.EVM,
      },
      base: { chainId: "8453", networkName: "base", chainType: ChainType.EVM },
      arbitrum: {
        chainId: "42161",
        networkName: "arbitrum",
        chainType: ChainType.EVM,
      },
    };
    return chains[chainName.toLowerCase()];
  }),
  getToken: mock().mockImplementation((chain: any, tokenSymbol: string) => {
    const tokens: Record<string, any> = {
      eth: {
        address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
        symbol: "ETH",
        decimals: 18,
        enabled: true,
        isNative: true,
      },
      uni: {
        address: "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984",
        symbol: "UNI",
        decimals: 18,
        enabled: true,
        isNative: false,
      },
      usdc: {
        address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
        symbol: "USDC",
        decimals: 6,
        enabled: true,
        isNative: false,
      },
    };
    return tokens[tokenSymbol.toLowerCase()];
  }),
  getEVMSignerForChain: mock().mockResolvedValue({
    getAddress: mock().mockResolvedValue(
      "0x742D35Cc6634c0532925A3b844BC9E7595f6F269"
    ),
  }),
  getRoute: mockGetRoute,
  executeRoute: mockExecuteRoute,
};

// Mock the initSquidRouterProvider function
const mockInitSquidRouterProvider = mock().mockResolvedValue(mockSquidProvider);

// Mock the module before importing
mock.module("../providers/squidRouter", () => ({
  initSquidRouterProvider: mockInitSquidRouterProvider,
  SquidRouterProvider: class {},
  squidRouterProvider: {
    name: "squidRouter",
    get: mock(),
  },
}));

// Import the action after mocking
const { xChainSwapAction } = await import("../actions/xChainSwap");

describe("xChainSwapAction", () => {
  let mockRuntime: any;
  let mockMessage: any;
  let mockCallback: any;

  beforeEach(() => {
    mockLogger();

    mockRuntime = createMockRuntime({
      useModel: mock().mockResolvedValue(`<response>
                <fromToken>ETH</fromToken>
                <toToken>ETH</toToken>
                <fromChain>ethereum</fromChain>
                <toChain>base</toChain>
                <amount>1</amount>
                <toAddress>0x742D35Cc6634c0532925A3b844BC9E7595f6F269</toAddress>
            </response>`),
    });

    mockMessage = createMockMemory({
      content: {
        text: "Bridge 1 ETH from Ethereum to Base",
      },
    });

    mockCallback = mock();

    // Reset all mocks
    mockInit.mockClear();
    mockSquidProvider.getChain.mockClear();
    mockSquidProvider.getToken.mockClear();
    mockGetRoute.mockClear();
    mockExecuteRoute.mockClear();
    mockInitSquidRouterProvider.mockClear();
  });

  describe("Basic Properties", () => {
    it("should have correct name", () => {
      expect(xChainSwapAction.name).toBe("X_CHAIN_SWAP");
    });

    it("should have description", () => {
      expect(xChainSwapAction.description).toContain(
        "Swaps tokens across chains"
      );
    });

    it("should have template", () => {
      expect(xChainSwapAction.template).toContain("{{recentMessages}}");
    });

    it("should have examples", () => {
      expect(xChainSwapAction.examples).toBeDefined();
      expect(xChainSwapAction.examples.length).toBeGreaterThan(0);
    });

    it("should have similes", () => {
      expect(xChainSwapAction.similes).toContain("CROSS_CHAIN_SWAP");
      expect(xChainSwapAction.similes).toContain("BRIDGE");
    });
  });

  describe("Validate Function", () => {
    it("should validate successfully with correct config", async () => {
      const result = await xChainSwapAction.validate(mockRuntime);
      expect(result).toBe(true);
    });

    it("should throw error with missing config", async () => {
      mockRuntime.getSetting.mockImplementation((key: string) => {
        if (key === "SQUID_INTEGRATOR_ID") return undefined;
        return "test-value";
      });

      await expect(xChainSwapAction.validate(mockRuntime)).rejects.toThrow();
    });
  });

  describe("Handler Function", () => {
    it("should successfully execute a cross-chain swap", async () => {
      const result = await xChainSwapAction.handler(
        mockRuntime,
        mockMessage,
        undefined,
        {},
        mockCallback
      );

      expect(result).toBe(true);
      expect(mockInit).toHaveBeenCalled();
      expect(mockSquidProvider.getChain).toHaveBeenCalledWith("ethereum");
      expect(mockSquidProvider.getChain).toHaveBeenCalledWith("base");
      expect(mockCallback).toHaveBeenCalledWith({
        text: expect.stringContaining("Swap completed successfully"),
        content: {},
      });
    });

    it("should handle invalid XML response", async () => {
      mockRuntime.useModel.mockResolvedValue("Invalid XML");

      const result = await xChainSwapAction.handler(
        mockRuntime,
        mockMessage,
        undefined,
        {},
        mockCallback
      );

      expect(result).toBe(false);
      expect(mockCallback).toHaveBeenCalledWith({
        text: "Unable to process cross-chain swap request. Invalid response format.",
        content: { error: "Failed to parse response" },
      });
    });

    it("should handle invalid swap content", async () => {
      mockRuntime.useModel.mockResolvedValue(`<response>
                <fromToken>ETH</fromToken>
            </response>`);

      const result = await xChainSwapAction.handler(
        mockRuntime,
        mockMessage,
        undefined,
        {},
        mockCallback
      );

      expect(result).toBe(false);
      expect(mockCallback).toHaveBeenCalledWith({
        text: "Unable to process cross-chain swap request. Invalid content provided.",
        content: { error: "Invalid cross-chain swap content" },
      });
    });

    it("should handle unsupported chain", async () => {
      mockRuntime.useModel.mockResolvedValue(`<response>
                <fromToken>ETH</fromToken>
                <toToken>ETH</toToken>
                <fromChain>unsupported</fromChain>
                <toChain>base</toChain>
                <amount>1</amount>
                <toAddress>0x742D35Cc6634c0532925A3b844BC9E7595f6F269</toAddress>
            </response>`);

      mockSquidProvider.getChain.mockReturnValue(undefined);

      const result = await xChainSwapAction.handler(
        mockRuntime,
        mockMessage,
        undefined,
        {},
        mockCallback
      );

      expect(result).toBe(false);
      expect(mockCallback).toHaveBeenCalledWith({
        text: expect.stringContaining("Error during cross-chain swap"),
        content: { error: expect.stringContaining("not supported") },
      });
    });

    it("should handle unsupported token", async () => {
      mockSquidProvider.getToken.mockReturnValue(undefined);

      const result = await xChainSwapAction.handler(
        mockRuntime,
        mockMessage,
        undefined,
        {},
        mockCallback
      );

      expect(result).toBe(false);
      expect(mockCallback).toHaveBeenCalledWith({
        text: expect.stringContaining("Error during cross-chain swap"),
        content: { error: expect.stringContaining("not supported") },
      });
    });

    it("should use default toAddress when not provided", async () => {
      mockRuntime.useModel.mockResolvedValue(`<response>
                <fromToken>ETH</fromToken>
                <toToken>ETH</toToken>
                <fromChain>ethereum</fromChain>
                <toChain>base</toChain>
                <amount>1</amount>
                <toAddress>null</toAddress>
            </response>`);

      const result = await xChainSwapAction.handler(
        mockRuntime,
        mockMessage,
        undefined,
        {},
        mockCallback
      );

      // Simply check that the handler was called
      expect(mockCallback).toHaveBeenCalled();
    });

    it("should handle transaction execution errors", async () => {
      // Use an invalid chain to trigger an error
      mockRuntime.useModel.mockResolvedValue(`<response>
                <fromToken>ETH</fromToken>
                <toToken>ETH</toToken>
                <fromChain>invalidchain</fromChain>
                <toChain>base</toChain>
                <amount>1</amount>
                <toAddress>0x742D35Cc6634c0532925A3b844BC9E7595f6F269</toAddress>
            </response>`);

      const result = await xChainSwapAction.handler(
        mockRuntime,
        mockMessage,
        undefined,
        {},
        mockCallback
      );

      expect(result).toBe(false);
      expect(mockCallback).toHaveBeenCalled();
      const callArg = mockCallback.mock.calls[0][0];
      expect(callArg.text).toContain("Error during cross-chain swap");
      expect(callArg.content.error).toBeDefined();
    });

    it("should skip approval for native tokens", async () => {
      const approveSpendingSpy = spyOn(console, "log");

      await xChainSwapAction.handler(
        mockRuntime,
        mockMessage,
        undefined,
        {},
        mockCallback
      );

      // Should not see approval log for native ETH
      expect(approveSpendingSpy).not.toHaveBeenCalledWith(
        expect.stringContaining("Approved")
      );
    });

    it("should approve spending for non-native tokens", async () => {
      mockRuntime.useModel.mockResolvedValue(`<response>
                <fromToken>UNI</fromToken>
                <toToken>USDC</toToken>
                <fromChain>ethereum</fromChain>
                <toChain>base</toChain>
                <amount>100</amount>
                <toAddress>0x742D35Cc6634c0532925A3b844BC9E7595f6F269</toAddress>
            </response>`);

      const result = await xChainSwapAction.handler(
        mockRuntime,
        mockMessage,
        undefined,
        {},
        mockCallback
      );

      // Just verify the handler was called
      expect(mockCallback).toHaveBeenCalled();
    });

    it("should respect API throttle interval", async () => {
      mockRuntime.getSetting.mockImplementation((key: string) => {
        if (key === "SQUID_API_THROTTLE_INTERVAL") return "100";
        return createMockRuntime().getSetting(key);
      });

      const result = await xChainSwapAction.handler(
        mockRuntime,
        mockMessage,
        undefined,
        {},
        mockCallback
      );

      // Just verify the handler was called
      expect(mockCallback).toHaveBeenCalled();
    });

    it("should compose state when state is not provided", async () => {
      await xChainSwapAction.handler(
        mockRuntime,
        mockMessage,
        undefined,
        {},
        mockCallback
      );

      expect(mockRuntime.composeState).toHaveBeenCalledWith(mockMessage);
    });

    it("should update state when state is provided", async () => {
      const mockState = createMockState();

      await xChainSwapAction.handler(
        mockRuntime,
        mockMessage,
        mockState as any,
        {},
        mockCallback
      );

      expect(mockRuntime.composeState).toHaveBeenCalledWith(mockMessage, [
        "RECENT_MESSAGES",
      ]);
    });
  });
});
