import { startProcess, getProcessStatus } from "./process";
import { Batch } from "./models/types";
import { EnoFactory } from "./EnoFactory";
import { IEnSrvOptions } from "./IEnSrvOptions";
import * as Send from "./send";
import { delay, map, of, switchMap, throwError, TimeoutError } from "rxjs";

const testEnSrvOptions: IEnSrvOptions = {
  enSrvUrl: "http://example.com",
  namespace: "myNameSpace",
};

describe("process", () => {
  it("Should start a process", (done) => {
    let callCount = 0;
    spyOn(Send, "send").and.callFake((batch: Batch) => {
      if (callCount === 0) {
        // Empty batch for session initialization
        expect(batch.length).toBe(0);
        callCount++;
        return of([]);
      }

      expect(batch.length).toBe(1);
      expect(batch[0].getType()).toBe("op/process");
      expect(batch[0].getFieldStringValue("op/process/process")).toBe(
        "test-process-tip"
      );
      expect(batch[0].getFieldJsonValue("op/process/inline-vars")).toEqual({
        myinputkey: ["myinputval"],
      });

      const enoFactory = new EnoFactory("response/process");
      enoFactory.setSecurity("security/policy/everyone");
      enoFactory.setField("response/process/op-tip", [batch[0].tip]);
      enoFactory.setField("response/process/inline-vars", [
        JSON.stringify({ myoutputkey: ["myoutputval"] }),
      ]);
      enoFactory.setField("response/process/finished", ["true"]);
      const responseEno = enoFactory.makeEno();

      return of([responseEno]);
    });

    startProcess("test-process-tip", testEnSrvOptions, {
      waitForFinish: true,
      inputVars: { myinputkey: ["myinputval"] },
    }).subscribe({
      next: (response) => {
        expect(response.isFinished).toBeTruthy();
        expect(response.outputVars).toEqual({ myoutputkey: ["myoutputval"] });
        done();
      },
      error: (err) => {
        throw err;
      },
    });
  });

  it("Should not start a non-existent process", (done) => {
    const enoFactory = new EnoFactory("error");
    enoFactory.setSecurity("security/policy/local");
    enoFactory.setField("error/message/tip", ["error/message/eno/not-found"]);
    const eno = enoFactory.makeEno();

    let callCount = 0;
    spyOn(Send, "send").and.callFake((batch: Batch) => {
      if (callCount === 0) {
        // Empty batch for session initialization
        expect(batch.length).toBe(0);
        callCount++;
        return of([]);
      }
      return of([eno]);
    });

    startProcess("test-process-tip", testEnSrvOptions, {
      waitForFinish: true,
      inputVars: { myinputkey: ["myinputval"] },
    }).subscribe({
      next: (_) => {
        throw "Should not have executed process";
      },
      error: (err) => {
        expect(err.message).toContain("error/message/eno/not-found");
        done();
      },
    });
  });

  it("Should get status of a process", (done) => {
    const enoFactory = new EnoFactory("response/process");
    enoFactory.setSecurity("security/policy/everyone");
    enoFactory.setField("response/process/op-tip", ["test-process-op-tip"]);
    enoFactory.setField("response/process/inline-vars", [
      JSON.stringify({ myoutputkey: ["myoutputval"] }),
    ]);
    enoFactory.setField("response/process/finished", ["true"]);
    const responseEno = enoFactory.makeEno();

    const sendSpy = spyOn(Send, "send").and.returnValue(of([responseEno]));

    getProcessStatus("test-process-op-tip", testEnSrvOptions).subscribe({
      next: (processResponse) => {
        expect(sendSpy).toHaveBeenCalledWith(
          jasmine.arrayContaining([
            jasmine.objectContaining({
              source: jasmine.objectContaining({
                type: "op/process/status",
                security: "security/policy/op",
                field: [
                  {
                    tip: "op/process/status:op-tip",
                    value: ["test-process-op-tip"],
                  },
                ],
              }),
            }),
          ]),
          testEnSrvOptions
        );
        expect(processResponse).toEqual({
          operationTip: "test-process-op-tip",
          responseTip: responseEno.tip,
          outputVars: { myoutputkey: ["myoutputval"] },
          isFinished: true,
        });
        done();
      },
      error: (err) => {
        console.error(err);
        fail();
        done();
      },
    });
  });

  it("Should fail to get status of a process", (done) => {
    spyOn(Send, "send").and.returnValue(of([]));

    getProcessStatus("test-process-op-tip", testEnSrvOptions).subscribe({
      next: () => {
        fail();
        done();
      },
      error: (err) => {
        expect(err.message).toBe("error/message/server/internal");
        done();
      },
    });
  });

  it("Should retry when not finished", (done) => {
    let callCount = 0;

    const sendSpy = spyOn(Send, "send").and.callFake((batch) => {
      const enoFactory = new EnoFactory("response/process");
      enoFactory.setSecurity("security/policy/everyone");

      if (callCount === 0) {
        // Empty batch for session initialization
        expect(batch.length).toBe(0);
        callCount++;
        return of([]);
      } else if (callCount === 1) {
        enoFactory.setField("response/process/op-tip", [batch[0].tip]);
        enoFactory.setField("response/process/finished", ["false"]);
      } else if (callCount === 2) {
        enoFactory.setField(
          "response/process/op-tip",
          batch[0].getFieldValues("op/process/status:op-tip")
        );
        enoFactory.setField("response/process/finished", ["false"]);
      } else if (callCount === 3) {
        enoFactory.setField(
          "response/process/op-tip",
          batch[0].getFieldValues("op/process/status:op-tip")
        );
        enoFactory.setField("response/process/finished", ["true"]);
      }
      callCount++;
      return of([enoFactory.makeEno()]);
    });

    startProcess("test-process-tip", testEnSrvOptions, {
      waitForFinish: true,
      retryDelayMs: 1,
      retryAttempts: 5,
    }).subscribe({
      next: (response) => {
        expect(sendSpy).toHaveBeenCalledTimes(4);
        expect(response.isFinished).toBeTrue();
        done();
      },
      error: (err) => {
        console.error(err);
        fail();
        done();
      },
    });
  });

  it("Should exhaust attempts when not finished", (done) => {
    let callCount = 0;

    const sendSpy = spyOn(Send, "send").and.callFake((batch) => {
      const enoFactory = new EnoFactory("response/process");
      enoFactory.setSecurity("security/policy/everyone");

      if (callCount === 0) {
        // Empty batch for session initialization
        expect(batch.length).toBe(0);
        callCount++;
        return of([]);
      } else if (callCount === 1) {
        enoFactory.setField("response/process/op-tip", [batch[0].tip]);
        enoFactory.setField("response/process/finished", ["false"]);
      } else if (callCount === 2) {
        enoFactory.setField(
          "response/process/op-tip",
          batch[0].getFieldValues("op/process/status:op-tip")
        );
        enoFactory.setField("response/process/finished", ["false"]);
      } else if (callCount === 3) {
        enoFactory.setField(
          "response/process/op-tip",
          batch[0].getFieldValues("op/process/status:op-tip")
        );
        enoFactory.setField("response/process/finished", ["true"]);
      }
      callCount++;
      return of([enoFactory.makeEno()]);
    });

    startProcess("test-process-tip", testEnSrvOptions, {
      waitForFinish: true,
      retryDelayMs: 1,
      retryAttempts: 1,
    }).subscribe({
      next: () => {
        fail();
        done();
      },
      error: (err) => {
        expect(sendSpy).toHaveBeenCalledTimes(3);
        expect(err.message).toBe(
          "Too many attempts waiting for process to finish"
        );
        done();
      },
    });
  });

  it("Should not retry if not waiting", (done) => {
    let callCount = 0;

    const sendSpy = spyOn(Send, "send").and.callFake((batch) => {
      const enoFactory = new EnoFactory("response/process");
      enoFactory.setSecurity("security/policy/everyone");
      if (callCount === 0) {
        // Empty batch for session initialization
        expect(batch.length).toBe(0);
        callCount++;
        return of([]);
      } else if (callCount === 1) {
        enoFactory.setField("response/process/op-tip", [batch[0].tip]);
        enoFactory.setField("response/process/finished", ["false"]);
      } else if (callCount === 2) {
        enoFactory.setField(
          "response/process/op-tip",
          batch[0].getFieldValues("op/process/status:op-tip")
        );
        enoFactory.setField("response/process/finished", ["false"]);
      } else if (callCount === 3) {
        enoFactory.setField(
          "response/process/op-tip",
          batch[0].getFieldValues("op/process/status:op-tip")
        );
        enoFactory.setField("response/process/finished", ["true"]);
      }
      callCount++;
      return of([enoFactory.makeEno()]);
    });

    startProcess("test-process-tip", testEnSrvOptions, {
      waitForFinish: false,
      retryDelayMs: 1,
      retryAttempts: 5,
    }).subscribe({
      next: (response) => {
        expect(sendSpy).toHaveBeenCalledTimes(2);
        expect(response.isFinished).toBeFalse();
        done();
      },
      error: (err) => {
        console.error(err);
        fail();
        done();
      },
    });
  });

  it("Should fetch the status if the process started but failed to respond", (done) => {
    let callCount = 0;

    const sendSpy = spyOn(Send, "send").and.callFake((batch) => {
      const enoFactory = new EnoFactory("response/process");
      enoFactory.setSecurity("security/policy/everyone");

      if (callCount === 0) {
        // Empty batch for session initialization
        expect(batch.length).toBe(0);
        callCount++;
        return of([]);
      } else if (callCount === 1) {
        callCount++;
        return throwError(() => new Error("Deliberate error"));
      } else if (callCount === 2) {
        enoFactory.setField(
          "response/process/op-tip",
          batch[0].getFieldValues("op/process/status:op-tip")
        );
        enoFactory.setField("response/process/finished", ["false"]);
      } else if (callCount === 3) {
        enoFactory.setField(
          "response/process/op-tip",
          batch[0].getFieldValues("op/process/status:op-tip")
        );
        enoFactory.setField("response/process/finished", ["true"]);
      }
      callCount++;
      return of([enoFactory.makeEno()]);
    });

    startProcess("test-process-tip", testEnSrvOptions, {
      waitForFinish: true,
      retryDelayMs: 1,
      retryAttempts: 5,
    }).subscribe({
      next: (response) => {
        expect(sendSpy).toHaveBeenCalledTimes(4);
        expect(response.isFinished).toBeTrue();
        done();
      },
      error: (err) => {
        console.error(err);
        fail();
        done();
      },
    });
  });

  it("Should fetch the status if the process did not start and failed to respond", (done) => {
    let callCount = 0;
    const sendSpy = spyOn(Send, "send").and.callFake((batch) => {
      if (callCount === 0) {
        // Empty batch for session initialization
        expect(batch.length).toBe(0);
        callCount++;
        return of([]);
      }
      return throwError(() => new Error("Deliberate error"));
    });

    startProcess("test-process-tip", testEnSrvOptions, {
      waitForFinish: true,
      retryDelayMs: 1,
      retryAttempts: 5,
    }).subscribe({
      next: (response) => {
        fail();
        done();
      },
      error: (err) => {
        expect(sendSpy).toHaveBeenCalledTimes(3);
        done();
      },
    });
  });

  it("Should fetch the status if the op/process started but timed out", (done) => {
    let callCount = 0;
    const enoFactory = new EnoFactory("response/process");
    enoFactory.setSecurity("security/policy/everyone");
    const sendSpy = spyOn(Send, "send").and.callFake((batch) => {
      if (callCount === 0) {
        // Empty batch for session initialization
        expect(batch.length).toBe(0);
        callCount++;
        return of([]);
      } else if (callCount === 1 && batch[0].getType() === 'op/process') {
        enoFactory.setField('response/process/op-tip', [batch[0].tip]);
        callCount++;
        return of(null).pipe(
          delay(3000),
          map(() => [enoFactory.makeEno()])
        );
      } else if (batch[0].getType() === 'op/process/status') {
        return of([enoFactory.makeEno()]);
      }
      return throwError(() => new Error('Unexpected operation'));
    });

    startProcess("test-process-tip", testEnSrvOptions, {
      waitForFinish: false,
      retryDelayMs: 1,
      retryAttempts: 1,
      timeoutMs: 2000,
    }).subscribe({
      next: (response) => {
        expect(sendSpy).toHaveBeenCalledTimes(3);
        done();
      },
      error: (err) => {
        fail();
        done();
      },
    });
  });

  // Table-driven test for session initialization failures
  [
    {
      description: "timeout error",
      errorFactory: () => throwError(() => new TimeoutError()),
    },
    {
      description: "deliberate exception",
      errorFactory: () => throwError(() => new Error("Deliberate session initialization error")),
    },
    {
      description: "503 status code with internal server error ENO",
      errorFactory: () => {
        const enoFactory = new EnoFactory("error");
        enoFactory.setSecurity("security/policy/everyone");
        enoFactory.setField("error/message/tip", ["error/message/server/internal"]);
        const errorEno = enoFactory.makeEno();
        return of([errorEno]);
      },
      isEnoResponse: true,
    },
  ].forEach(({ errorFactory, description, isEnoResponse }) => {
    it(`Should proceed with process execution on session initialization failure: ${description}`, (done) => {
      const opts = { enSrvUrl: "http://example.com", namespace: "myNameSpace" };

      let callCount = 0;
      const sendSpy = spyOn(Send, "send").and.callFake((batch: Batch, options: any) => {
        if (callCount === 0) {
          expect(batch.length).toBe(0);
          expect(options.maintainInitialSessionToken).toBe(true);
          callCount++;

          return errorFactory();
        }

        // Second call should be the actual process execution
        expect(batch.length).toBe(1);
        expect(batch[0].getType()).toBe("op/process");
        expect(batch[0].getFieldStringValue("op/process/process")).toBe("test-process-tip");

        expect(options.maintainInitialSessionToken).toBe(true);

        const enoFactory = new EnoFactory("response/process");
        enoFactory.setSecurity("security/policy/everyone");
        enoFactory.setField("response/process/op-tip", [batch[0].tip]);
        enoFactory.setField("response/process/finished", ["true"]);
        const responseEno = enoFactory.makeEno();

        return of([responseEno]);
      });

      startProcess("test-process-tip", opts, { waitForFinish: true }).subscribe({
        next: (response) => {
          // Verify that both calls were made: session init (failed) and process execution (succeeded)
          expect(sendSpy).toHaveBeenCalledTimes(2);

          // Verify first call was session initialization (empty batch)
          expect(sendSpy.calls.argsFor(0)[0]).toEqual([]);

          // Verify second call was process execution
          expect(sendSpy.calls.argsFor(1)[0].length).toBe(1);
          expect(sendSpy.calls.argsFor(1)[0][0].getType()).toBe("op/process");

          expect(response.isFinished).toBeTruthy();
          done();
        },
        error: (err) => {
          fail(`Should not have failed: ${err.message}`);
          done();
        },
      });
    });
  });

  it("Should skip session initialization when sessionToken is already set", (done) => {
    const opts = {
      enSrvUrl: "http://example.com",
      namespace: "myNameSpace",
      sessionToken: "existing-session-token",
    };

    const sendSpy = spyOn(Send, "send").and.callFake((batch: Batch, options: any) => {
      // The batch is non-empty. Empty batch is only sent for session initialization.
      expect(batch.length).toBe(1);
      expect(batch[0].getType()).toBe("op/process");
      expect(batch[0].getFieldStringValue("op/process/process")).toBe("test-process-tip");
      expect(options.maintainInitialSessionToken).toBe(true);

      const enoFactory = new EnoFactory("response/process");
      enoFactory.setSecurity("security/policy/everyone");
      enoFactory.setField("response/process/op-tip", [batch[0].tip]);
      enoFactory.setField("response/process/finished", ["true"]);
      return of([enoFactory.makeEno()]);
    });

    startProcess("test-process-tip", opts, { waitForFinish: true }).subscribe({
      next: (response) => {
        // Assert there was no session initialization
        expect(sendSpy).toHaveBeenCalledTimes(1);
        expect(sendSpy.calls.argsFor(0)[0].length).toBe(1);
        expect(sendSpy.calls.argsFor(0)[0][0].getType()).toBe("op/process");

        expect(response.isFinished).toBeTruthy();
        done();
      },
      error: (err) => {
        fail(`Should not have failed: ${err.message}`);
        done();
      },
    });
  });

  // Table-driven test for maintainInitialSessionToken reversion
  [false, true, undefined].forEach((originalValue) => {
    [
      {
        description: "successful process completion",
        setupSendSpy: (sendSpy: jasmine.Spy) => {
          let callCount = 0;
          sendSpy.and.callFake((batch: Batch) => {
            if (callCount === 0) {
              // Empty batch for session initialization
              callCount++;
              return of([]);
            }

            const enoFactory = new EnoFactory("response/process");
            enoFactory.setSecurity("security/policy/everyone");
            enoFactory.setField("response/process/op-tip", [batch[0].tip]);
            enoFactory.setField("response/process/finished", ["true"]);
            return of([enoFactory.makeEno()]);
          });
        },
        expectError: false,
      },
      {
        description: "process execution error",
        setupSendSpy: (sendSpy: jasmine.Spy) => {
          let callCount = 0;
          sendSpy.and.callFake((batch: Batch) => {
            if (callCount === 0) {
              // Empty batch for session initialization
              callCount++;
              return of([]);
            }

            return throwError(() => new Error("Process execution failed"));
          });
        },
        expectError: true,
      },
      {
        description: "timeout during process execution",
        setupSendSpy: (sendSpy: jasmine.Spy) => {
          let callCount = 0;
          sendSpy.and.callFake((batch: Batch) => {
            if (callCount === 0) {
              // Empty batch for session initialization
              callCount++;
              return of([]);
            }

            return of(null).pipe(
              delay(3000),
              map(() => [])
            );
          });
        },
        expectError: true,
      },
      {
        description: "existing session token present",
        setupSendSpy: (sendSpy: jasmine.Spy) => {
          sendSpy.and.callFake((batch: Batch) => {
            // No session initialization - directly process execution
            const enoFactory = new EnoFactory("response/process");
            enoFactory.setSecurity("security/policy/everyone");
            enoFactory.setField("response/process/op-tip", [batch[0].tip]);
            enoFactory.setField("response/process/finished", ["true"]);
            return of([enoFactory.makeEno()]);
          });
        },
        expectError: false,
        hasExistingSessionToken: true,
      },
    ].forEach(({ description, setupSendSpy, expectError, hasExistingSessionToken }) => {
      it(`Should revert maintainInitialSessionToken on ${description} with original ${originalValue}`, (done) => {
        jasmine.DEFAULT_TIMEOUT_INTERVAL = 500000;
        const opts: any = {
          enSrvUrl: "http://example.com",
          namespace: "myNameSpace",
        };

        if (originalValue !== undefined) {
          opts.maintainInitialSessionToken = originalValue;
        }

        if (hasExistingSessionToken) {
          opts.sessionToken = "existing-session-token";
        }

        const sendSpy = spyOn(Send, "send");
        setupSendSpy(sendSpy);

        const checkAndComplete = () => {
          setTimeout(() => {
            expect(opts.maintainInitialSessionToken).toBe(originalValue);
            done();
          }, 0);
        };

        startProcess("test-process-tip", opts, {
          waitForFinish: false,
          timeoutMs: 1000,
        }).subscribe({
          next: () => {
            if (expectError) {
              fail("Expected an error but got success");
            }
          },
          error: () => {
            if (!expectError) {
              fail("Expected success but got an error");
            }
            checkAndComplete();
          },
          complete: () => {
            if (!expectError) {
              checkAndComplete();
            }
          },
        });
      });
    });
  });
});
