import { Dissected, FieldType } from "@hamradio/packet";
import { describe, expect, it } from "vitest";

import { Address, Frame } from "../src/frame";
import {
  DataType,
  type ITimestamp,
  type MessagePayload,
  MicEPayload,
  type ObjectPayload,
  type Payload,
  type PositionPayload,
  type StatusPayload
} from "../src/frame.types";
import Timestamp from "../src/timestamp";

// Address parsing: split by method
describe("Address.parse", () => {
  it("should parse callsign without SSID", () => {
    const result = Address.parse("NOCALL");
    expect(result).toEqual({ call: "NOCALL", ssid: "", isRepeated: false });
  });
});

describe("Address.fromString", () => {
  it("should parse callsign with SSID", () => {
    const result = Address.fromString("NOCALL-1");
    expect(result).toEqual({ call: "NOCALL", ssid: "1", isRepeated: false });
  });

  it("should parse repeated address", () => {
    const result = Address.fromString("WA1PLE-4*");
    expect(result).toEqual({ call: "WA1PLE", ssid: "4", isRepeated: true });
  });

  it("should parse address without SSID but with repeat marker", () => {
    const result = Address.fromString("WIDE1*");
    expect(result).toEqual({ call: "WIDE1", ssid: "", isRepeated: true });
  });
});

// Frame constructor first
describe("Frame.constructor", () => {
  it("returns a Frame instance from Frame.fromString", () => {
    const data = "W1AW>APRS:>Status message";
    const result = Frame.fromString(data);
    expect(result).toBeInstanceOf(Object);
  });
});

// Frame properties / instance methods
describe("Frame.getDataTypeIdentifier", () => {
  it("returns @ for position identifier", () => {
    const data = 'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!';
    const frame = Frame.fromString(data);
    expect(frame.getDataTypeIdentifier()).toBe("@");
  });

  it("returns ` for Mic-E identifier", () => {
    const data = "N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&'/'\"G:} KJ6TMS|!:&0'p|!w#f!|3";
    const frame = Frame.fromString(data);
    expect(frame.getDataTypeIdentifier()).toBe("`");
  });

  it("returns : for message identifier", () => {
    const data = "W1AW>APRS::KB1ABC-5 :Hello World";
    const frame = Frame.fromString(data);
    expect(frame.getDataTypeIdentifier()).toBe(":");
  });

  it("returns > for status identifier", () => {
    const data = "W1AW>APRS:>Status message";
    const frame = Frame.fromString(data);
    expect(frame.getDataTypeIdentifier()).toBe(">");
  });
});

describe("Frame.decode (basic)", () => {
  it("should call decode and return position payload", () => {
    const data = 'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!';
    const frame = Frame.fromString(data);
    const decoded = frame.decode() as PositionPayload;
    expect(decoded).not.toBeNull();
    expect(decoded?.type).toBe(DataType.PositionWithTimestampWithMessaging);
  });

  it("should handle various data type identifiers without throwing", () => {
    const testCases = [
      { data: "CALL>APRS:!4903.50N/07201.75W-", type: "!" },
      { data: "CALL>APRS:=4903.50N/07201.75W-", type: "=" },
      { data: "CALL>APRS:/092345z4903.50N/07201.75W>", type: "/" },
      { data: "CALL>APRS:>Status Text", type: ">" },
      { data: "CALL>APRS::ADDRESS  :Message", type: ":" },
      { data: "CALL>APRS:;OBJECT   *092345z4903.50N/07201.75W>", type: ";" },
      { data: "CALL>APRS:)ITEM!4903.50N/07201.75W-", type: ")" },
      { data: "CALL>APRS:?APRS?", type: "?" },
      { data: "CALL>APRS:T#001,123,456,789", type: "T" },
      { data: "CALL>APRS:_10090556c...", type: "_" },
      { data: "CALL>APRS:$GPRMC,...", type: "$" },
      { data: "CALL>APRS:<IGATE,MSG_CNT", type: "<" },
      { data: "CALL>APRS:{01", type: "{" },
      { data: "CALL>APRS:}W1AW>APRS:test", type: "}" }
    ];
    for (const testCase of testCases) {
      const frame = Frame.fromString(testCase.data);
      expect(frame.getDataTypeIdentifier()).toBe(testCase.type);
      expect(() => frame.decode()).not.toThrow();
    }
  });
});

// Static functions
describe("Frame.fromString", () => {
  it("parses APRS position frame (test vector 1)", () => {
    const data = 'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!';
    const result = Frame.fromString(data);
    expect(result.source).toEqual({
      call: "NOCALL",
      ssid: "1",
      isRepeated: false
    });
    expect(result.destination).toEqual({
      call: "APRS",
      ssid: "",
      isRepeated: false
    });
    expect(result.path).toHaveLength(1);
    expect(result.path[0]).toEqual({
      call: "WIDE1",
      ssid: "1",
      isRepeated: false
    });
    expect(result.payload).toBe('@092345z/:*E";qZ=OMRC/A=088132Hello World!');
  });

  it("parses APRS position frame without messaging (test vector 3)", () => {
    const data = "N0CALL-7>APLRT1,qAO,DG2EAZ-10:!/4CkRP&-V>76Q";
    const result = Frame.fromString(data);
    expect(result.source).toEqual({
      call: "N0CALL",
      ssid: "7",
      isRepeated: false
    });
    expect(result.destination).toEqual({
      call: "APLRT1",
      ssid: "",
      isRepeated: false
    });
    expect(result.path).toHaveLength(2);

    const payload = result.decode(false);
    expect(payload).not.toBeNull();
  });

  it("parses APRS Mic-E frame with repeated digipeater (test vector 2)", () => {
    const data = "N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&'/'\"G:} KJ6TMS|!:&0'p|!w#f!|3";
    const result = Frame.fromString(data);
    expect(result.source).toEqual({
      call: "N83MZ",
      ssid: "",
      isRepeated: false
    });
    expect(result.destination).toEqual({
      call: "T2TQ5U",
      ssid: "",
      isRepeated: false
    });
    expect(result.path).toHaveLength(1);
    expect(result.path[0]).toEqual({
      call: "WA1PLE",
      ssid: "4",
      isRepeated: true
    });
    expect(result.payload).toBe("`c.l+@&'/'\"G:} KJ6TMS|!:&0'p|!w#f!|3");
  });

  it("parses frame with multiple path elements", () => {
    const data = "KB1ABC-5>APRS,WIDE1-1,WIDE2-2*,IGATE:!4903.50N/07201.75W-Test";
    const result = Frame.fromString(data);
    expect(result.source.call).toBe("KB1ABC");
    expect(result.path).toHaveLength(3);
    expect(result.payload).toBe("!4903.50N/07201.75W-Test");
  });

  it("parses frame with no path", () => {
    const data = "W1AW>APRS::STATUS:Testing";
    const result = Frame.fromString(data);
    expect(result.path).toHaveLength(0);
    expect(result.payload).toBe(":STATUS:Testing");
  });

  it("throws for frame without route separator", () => {
    const data = "NOCALL-1>APRS";
    expect(() => Frame.fromString(data)).toThrow("APRS: invalid frame, no route separator found");
  });

  it("throws for frame with invalid addresses", () => {
    const data = "NOCALL:payload";
    expect(() => Frame.fromString(data)).toThrow("APRS: invalid addresses in route");
  });
});

// Timestamp class (toDate / constructors)
describe("Timestamp.toDate", () => {
  it("creates DHM timestamp and converts to Date", () => {
    const ts = new Timestamp(14, 30, "DHM", { day: 15, zulu: true });
    expect(ts.hours).toBe(14);
    expect(ts.minutes).toBe(30);
    expect(ts.day).toBe(15);
    const date = ts.toDate();
    expect(date).toBeInstanceOf(Date);
    expect(date.getUTCDate()).toBe(15);
  });

  it("creates HMS timestamp and converts to Date", () => {
    const ts = new Timestamp(12, 45, "HMS", { seconds: 30, zulu: true });
    const date = ts.toDate();
    expect(date).toBeInstanceOf(Date);
    expect(date.getUTCHours()).toBe(12);
  });

  it("creates MDHM timestamp and converts to Date", () => {
    const ts = new Timestamp(16, 20, "MDHM", { month: 3, day: 5, zulu: false });
    const date = ts.toDate();
    expect(date).toBeInstanceOf(Date);
    expect(date.getMonth()).toBe(2);
  });
});

// Private decode functions (alphabetical order)
describe("Frame.decodeMicE", () => {
  it("decodes a basic Mic-E packet (current format)", () => {
    const data = "N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&'/'\"G:} KJ6TMS|!:&0'p|!w#f!|3";
    const frame = Frame.fromString(data);
    const decoded = frame.decode() as MicEPayload;
    expect(decoded).not.toBeNull();
    expect(decoded?.type).toBe(DataType.MicE);
  });

  it("decodes a Mic-E packet with old format (single quote)", () => {
    const data = "CALL>T2TQ5U:'c.l+@&'/'\"G:}";
    const frame = Frame.fromString(data);
    const decoded = frame.decode() as MicEPayload;
    expect(decoded).not.toBeNull();
    expect(decoded?.type).toBe(DataType.MicEOld);
  });
});

describe("Frame.decodeMessage", () => {
  it("decodes a standard APRS message with 9-char recipient field", () => {
    const raw = "W1AW>APRS::KB1ABC-5 :Hello World";
    const frame = Frame.fromString(raw);
    const decoded = frame.decode() as MessagePayload;
    expect(decoded).not.toBeNull();
    expect(decoded?.type).toBe(DataType.Message);
    expect(decoded?.addressee).toBe("KB1ABC-5");
    expect(decoded?.text).toBe("Hello World");
  });

  it("emits recipient and text sections when emitSections is true", () => {
    const raw = "W1AW>APRS::KB1ABC  :Test via spec";
    const frame = Frame.fromString(raw);
    const res = frame.decode(true) as {
      payload: MessagePayload | null;
      structure: Dissected;
    };
    expect(res.payload).not.toBeNull();
    expect(res.payload?.type).toBe(DataType.Message);
    const recipientSection = res.structure.find((s) => s.name === "recipient");
    const textSection = res.structure.find((s) => s.name === "text");
    expect(recipientSection).toBeDefined();
    expect(textSection).toBeDefined();
    expect(new TextDecoder().decode(recipientSection!.data).trim()).toBe("KB1ABC");
    expect(new TextDecoder().decode(textSection!.data)).toBe("Test via spec");
  });
});

describe("Frame.decodeObject", () => {
  it("decodes object payload with uncompressed position", () => {
    const data = "CALL>APRS:;OBJECT   *092345z4903.50N/07201.75W>Test object";
    const frame = Frame.fromString(data);
    const decoded = frame.decode() as ObjectPayload;
    expect(decoded).not.toBeNull();
    expect(decoded?.type).toBe(DataType.Object);
    if (decoded && decoded.type === DataType.Object) {
      expect(decoded.name).toBe("OBJECT");
      expect(decoded.alive).toBe(true);
    }
  });

  it("emits object sections when emitSections is true", () => {
    const data = "CALL>APRS:;OBJECT   *092345z4903.50N/07201.75W>Test object";
    const frame = Frame.fromString(data);
    const result = frame.decode(true) as {
      payload: ObjectPayload | null;
      structure: Dissected;
    };
    expect(result.payload).not.toBeNull();
    expect(result.structure.length).toBeGreaterThan(0);
  });
});

describe("Frame.decodePosition", () => {
  it("decodes position with timestamp and compressed format", () => {
    const data = 'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!';
    const frame = Frame.fromString(data);
    const decoded = frame.decode() as PositionPayload;
    expect(decoded).not.toBeNull();
    expect(decoded?.type).toBe(DataType.PositionWithTimestampWithMessaging);
  });

  it("decodes uncompressed position without timestamp", () => {
    const data = "KB1ABC>APRS:!4903.50N/07201.75W-Test message";
    const frame = Frame.fromString(data);
    const decoded = frame.decode() as PositionPayload;
    expect(decoded).not.toBeNull();
    expect(decoded?.type).toBe(DataType.PositionNoTimestampNoMessaging);
  });

  it("handles ambiguity masking in position", () => {
    const data = "CALL>APRS:!4903.5 N/07201.75W-";
    const frame = Frame.fromString(data);
    const decoded = frame.decode() as PositionPayload;
    expect(decoded).not.toBeNull();
  });

  it("should handle UTF-8 characters", () => {
    const data =
      "WB2OSZ-5>APDW17:!4237.14NS07120.83W#PHG7140 Did you know that APRS comments and messages can contain UTF-8 characters? アマチュア無線";
    const frame = Frame.fromString(data);
    const decoded = frame.decode() as PositionPayload;
    expect(decoded).not.toBeNull();
    expect(decoded?.type).toBe(DataType.PositionNoTimestampNoMessaging);
    expect(decoded?.position.comment).toContain("UTF-8 characters? アマチュア無線");
  });
});

describe("Frame.decodeStatus", () => {
  it("decodes a status payload with timestamp and Maidenhead", () => {
    const raw = "N0CALL>APRS:>120912zTesting status FN20";
    const frame = Frame.parse(raw);
    const res = frame.decode(true) as {
      payload: StatusPayload | null;
      structure: Dissected;
    };
    expect(res.payload).not.toBeNull();
    if (res.payload?.type !== DataType.Status) throw new Error("expected status payload");
    const payload = res.payload as StatusPayload & { timestamp?: ITimestamp };
    expect(payload.text).toBe("Testing status");
    expect(payload.maidenhead).toBe("FN20");
  });
});

// Packet dissection and sections
describe("Frame.decode (sections)", () => {
  it("emits routing sections when emitSections is true", () => {
    const data = "KB1ABC-5>APRS,WIDE1-1,WIDE2-2*:!4903.50N/07201.75W-Test";
    const frame = Frame.fromString(data);
    const result = frame.decode(true) as {
      payload: ObjectPayload | null;
      structure: Dissected;
    };
    expect(result.structure.length).toBeGreaterThan(0);
  });

  it("emits position payload sections when emitSections is true", () => {
    const data = "CALL>APRS:!4903.50N/07201.75W-Test message";
    const frame = Frame.fromString(data);
    const result = frame.decode(true) as {
      payload: PositionPayload | null;
      structure: Dissected;
    };
    expect(result.payload?.type).toBe(DataType.PositionNoTimestampNoMessaging);
  });
});

describe("Timestamp.toDate", () => {
  it("should create DHM timestamp and convert to Date", () => {
    const ts = new Timestamp(14, 30, "DHM", { day: 15, zulu: true });

    expect(ts.hours).toBe(14);
    expect(ts.minutes).toBe(30);
    expect(ts.day).toBe(15);
    expect(ts.format).toBe("DHM");
    expect(ts.zulu).toBe(true);

    const date = ts.toDate();
    expect(date).toBeInstanceOf(Date);
    expect(date.getUTCDate()).toBe(15);
    expect(date.getUTCHours()).toBe(14);
    expect(date.getUTCMinutes()).toBe(30);
  });

  it("should create HMS timestamp and convert to Date", () => {
    const ts = new Timestamp(12, 45, "HMS", { seconds: 30, zulu: true });

    expect(ts.hours).toBe(12);
    expect(ts.minutes).toBe(45);
    expect(ts.seconds).toBe(30);
    expect(ts.format).toBe("HMS");

    const date = ts.toDate();
    expect(date).toBeInstanceOf(Date);
    expect(date.getUTCHours()).toBe(12);
    expect(date.getUTCMinutes()).toBe(45);
    expect(date.getUTCSeconds()).toBe(30);
  });

  it("should create MDHM timestamp and convert to Date", () => {
    const ts = new Timestamp(16, 20, "MDHM", { month: 3, day: 5, zulu: false });

    expect(ts.hours).toBe(16);
    expect(ts.minutes).toBe(20);
    expect(ts.month).toBe(3);
    expect(ts.day).toBe(5);
    expect(ts.format).toBe("MDHM");
    expect(ts.zulu).toBe(false);

    const date = ts.toDate();
    expect(date).toBeInstanceOf(Date);
    expect(date.getMonth()).toBe(2); // 0-indexed
    expect(date.getDate()).toBe(5);
    expect(date.getHours()).toBe(16);
    expect(date.getMinutes()).toBe(20);
  });

  it("should handle DHM timestamp that is in the future (use previous month)", () => {
    const now = new Date();
    const futureDay = now.getUTCDate() + 5;

    const ts = new Timestamp(12, 0, "DHM", { day: futureDay, zulu: true });
    const date = ts.toDate();

    // Should be in the past or very close to now
    expect(date <= now).toBe(true);
  });

  it("should handle HMS timestamp that is in the future (use yesterday)", () => {
    const now = new Date();
    const futureHours = now.getUTCHours() + 2;

    if (futureHours < 24) {
      const ts = new Timestamp(futureHours, 0, "HMS", {
        seconds: 0,
        zulu: true
      });
      const date = ts.toDate();

      // Should be in the past
      expect(date <= now).toBe(true);
    }
  });

  it("should handle MDHM timestamp that is in the future (use last year)", () => {
    const now = new Date();
    const futureMonth = now.getMonth() + 2;

    if (futureMonth < 12) {
      const ts = new Timestamp(12, 0, "MDHM", {
        month: futureMonth + 1,
        day: 1,
        zulu: false
      });
      const date = ts.toDate();

      // Should be in the past
      expect(date <= now).toBe(true);
    }
  });
});

// Private decode functions and related parsing tests (alphabetical grouping)

describe("Frame.decodeMicE", () => {
  describe("Basic Mic-E frames", () => {
    it("should decode a basic Mic-E packet (current format)", () => {
      const data = "N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&'/'\"G:} KJ6TMS|!:&0'p|!w#f!|3";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as MicEPayload;

      expect(decoded).not.toBeNull();
      expect(decoded?.type).toBe(DataType.MicE);

      if (decoded && decoded.type === DataType.MicE) {
        expect(decoded.position).toBeDefined();
        expect(typeof decoded.position.latitude).toBe("number");
        expect(typeof decoded.position.longitude).toBe("number");
        expect(decoded.position.symbol).toBeDefined();
        expect(decoded.messageType).toBeDefined();
      }
    });

    it("should decode a Mic-E packet with old format (single quote)", () => {
      const data = "CALL>T2TQ5U:'c.l+@&'/'\"G:}";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as MicEPayload;

      expect(decoded).not.toBeNull();
      expect(decoded?.type).toBe(DataType.MicEOld);
    });
  });

  describe("Frame.decodeLatitude", () => {
    it("should decode latitude from numeric digits (0-9)", () => {
      const data = "CALL>123456:`c.l+@&'/'\"G:}";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as MicEPayload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === DataType.MicE) {
        expect(decoded.position.latitude).toBeCloseTo(12 + 34.56 / 60, 3);
      }
    });

    it("should decode latitude from letter digits (A-J)", () => {
      const data = "CALL>ABC0EF:`c.l+@&'/'\"G:}";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as MicEPayload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === DataType.MicE) {
        expect(decoded.position.latitude).toBeCloseTo(1 + 20.45 / 60, 3);
      }
    });

    it("should decode latitude with mixed digits and letters", () => {
      const data = "CALL>4AB2DE:`c.l+@&'/'\"G:}";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as MicEPayload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === DataType.MicE) {
        expect(decoded.position.latitude).toBeCloseTo(40 + 12.34 / 60, 3);
      }
    });

    it("should decode latitude for southern hemisphere", () => {
      const data = "CALL>4A0P0U:`c.l+@&'/'\"G:}";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as MicEPayload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === DataType.MicE) {
        expect(decoded.position.latitude).toBeLessThan(0);
      }
    });
  });

  describe("Longitude decoding from information field", () => {
    it("should decode longitude from information field", () => {
      const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as MicEPayload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === DataType.MicE) {
        expect(typeof decoded.position.longitude).toBe("number");
        expect(decoded.position.longitude).toBeGreaterThanOrEqual(-180);
        expect(decoded.position.longitude).toBeLessThanOrEqual(180);
      }
    });

    it("should handle eastern hemisphere longitude", () => {
      const data = "CALL>4ABPDE:`c.l+@&'/'\"G:}";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as MicEPayload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === DataType.MicE) {
        expect(typeof decoded.position.longitude).toBe("number");
      }
    });

    it("should handle longitude offset +100", () => {
      const data = "CALL>4ABCDP:`c.l+@&'/'\"G:}";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as MicEPayload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === DataType.MicE) {
        expect(typeof decoded.position.longitude).toBe("number");
        expect(Math.abs(decoded.position.longitude)).toBeGreaterThan(90);
      }
    });
  });

  describe("Speed and course decoding", () => {
    it("should decode speed from information field", () => {
      const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}Speed test";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === DataType.MicE) {
        if (decoded.position.speed !== undefined) {
          expect(decoded.position.speed).toBeGreaterThanOrEqual(0);
          expect(typeof decoded.position.speed).toBe("number");
        }
      }
    });

    it("should decode course from information field", () => {
      const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === DataType.MicE) {
        if (decoded.position.course !== undefined) {
          expect(decoded.position.course).toBeGreaterThanOrEqual(0);
          expect(decoded.position.course).toBeLessThan(360);
        }
      }
    });

    it("should not include zero speed in result", () => {
      const data = "CALL>4ABCDE:`\x1c\x1c\x1c\x1c\x1c\x1c/>}";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === DataType.MicE) {
        expect(decoded.position.speed).toBeUndefined();
      }
    });

    it("should not include zero or 360+ course in result", () => {
      const data = "CALL>4ABCDE:`c.l\x1c\x1c\x1c/>}";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as MicEPayload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === DataType.MicE) {
        if (decoded.position.course !== undefined) {
          expect(decoded.position.course).toBeGreaterThan(0);
          expect(decoded.position.course).toBeLessThan(360);
        }
      }
    });
  });

  describe("Symbol decoding", () => {
    it("should decode symbol table and code", () => {
      const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === DataType.MicE) {
        expect(decoded.position.symbol).toBeDefined();
        expect(decoded.position.symbol?.table).toBeDefined();
        expect(decoded.position.symbol?.code).toBeDefined();
        expect(typeof decoded.position.symbol?.table).toBe("string");
        expect(typeof decoded.position.symbol?.code).toBe("string");
      }
    });
  });

  describe("Altitude decoding", () => {
    it("should decode altitude from /A=NNNNNN format", () => {
      const data = "CALL>4ABCDE:`c.l+@&'//A=001234";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as MicEPayload;

      expect(decoded).not.toBeNull();
      expect(decoded.type).toBe(DataType.MicE);

      expect(decoded.position).toBeDefined();
      expect(decoded.position.altitude).toBeDefined();
      expect(decoded.position.altitude).toBeCloseTo(1234 * 0.3048, 1);
    });

    it("should decode altitude from base-91 format abc}", () => {
      const data = "N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&'/\"4T}KJ6TMS";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as MicEPayload;

      expect(decoded).not.toBeNull();
      expect(decoded.type).toBe(DataType.MicE);

      expect(decoded.position).toBeDefined();
      expect(decoded.position.altitude).toBeDefined();
      expect(decoded.position.comment).toBe("KJ6TMS");
      expect(decoded.position.altitude).toBeCloseTo(61, 1);
    });

    it("should prefer /A= format over base-91 when both present", () => {
      const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}}!!!/A=005000";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as MicEPayload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === DataType.MicE) {
        expect(decoded.position.altitude).toBeCloseTo(5000 * 0.3048, 1);
      }
    });

    it("should handle comment without altitude", () => {
      const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}Just a comment";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as MicEPayload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === DataType.MicE) {
        expect(decoded.position.altitude).toBeUndefined();
        expect(decoded.position.comment).toContain("Just a comment");
      }
    });
  });

  describe("Frame.decodeMessage", () => {
    it("should decode message type M0 (Off Duty)", () => {
      const data = "CALL>012345:`c.l+@&'/'\"G:}";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as MicEPayload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === DataType.MicE) {
        expect(decoded.messageType).toBe("M0: Off Duty");
      }
    });

    it("should decode message type M7 (Emergency)", () => {
      const data = "CALL>ABCDEF:`c.l+@&'/'\"G:}";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as MicEPayload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === DataType.MicE) {
        expect(decoded.messageType).toBeDefined();
        expect(typeof decoded.messageType).toBe("string");
      }
    });

    it("should decode standard vs custom message indicator", () => {
      const data = "CALL>ABCDEF:`c.l+@&'/'\"G:}";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as MicEPayload;

      expect(decoded).not.toBeNull();
    });
  });

  describe("Comment and telemetry", () => {
    it("should extract comment from remaining data", () => {
      const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}This is a test comment";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === DataType.MicE) {
        expect(decoded.position.comment).toContain("This is a test comment");
      }
    });

    it("should handle empty comment", () => {
      const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === DataType.MicE) {
        expect(decoded.position.comment).toBeDefined();
      }
    });
  });

  describe("Error handling", () => {
    it("should return null for destination address too short", () => {
      const data = "CALL>SHORT:`c.l+@&'/'\"G:}";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).toBeNull();
    });

    it("should return null for payload too short", () => {
      const data = "CALL>4ABCDE:`short";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).toBeNull();
    });

    it("should return null for invalid destination characters", () => {
      const data = "CALL>4@BC#E:`c.l+@&'/'\"G:}";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).toBeNull();
    });

    it("should handle exceptions gracefully", () => {
      const data = "CALL>4ABCDE:`\x00\x00\x00\x00\x00\x00\x00\x00";
      const frame = Frame.fromString(data);

      expect(() => frame.decode()).not.toThrow();
      const decoded = frame.decode() as MicEPayload;
      expect(decoded === null || decoded?.type === DataType.MicE).toBe(true);
    });
  });

  describe("Real-world test vectors", () => {
    it("should decode real Mic-E packet from test vector 2", () => {
      const data = "N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&'/'\"G:} KJ6TMS|!:&0'p|!w#f!|3";
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as MicEPayload;

      expect(decoded).not.toBeNull();
      expect(decoded?.type).toBe(DataType.MicE);

      if (decoded && decoded.type === DataType.MicE) {
        expect(decoded.position.latitude).toBeDefined();
        expect(decoded.position.longitude).toBeDefined();
        expect(decoded.position.symbol).toBeDefined();

        expect(Math.abs(decoded.position.latitude)).toBeLessThanOrEqual(90);
        expect(Math.abs(decoded.position.longitude)).toBeLessThanOrEqual(180);
      }
    });
  });
});

describe("Packet dissection with sections", () => {
  it("should emit routing sections when emitSections is true", () => {
    const data = "KB1ABC-5>APRS,WIDE1-1,WIDE2-2*:!4903.50N/07201.75W-Test";
    const frame = Frame.fromString(data);
    const result = frame.decode(true) as {
      payload: Payload;
      structure: Dissected;
    };

    expect(result).toHaveProperty("payload");
    expect(result).toHaveProperty("structure");
    expect(result.structure).toBeDefined();
    expect(result.structure.length).toBeGreaterThan(0);

    const routingSection = result.structure.find((s) => s.name === "routing");
    expect(routingSection).toBeDefined();
    expect(routingSection?.fields).toBeDefined();
    expect(routingSection?.fields?.length).toBeGreaterThan(0);

    const sourceField = routingSection?.fields?.find((a) => a.name === "source address");
    expect(sourceField).toBeDefined();
    expect(sourceField?.length).toBeGreaterThan(0);

    const destField = routingSection?.fields?.find((a) => a.name === "destination address");
    expect(destField).toBeDefined();
    expect(destField?.length).toBeGreaterThan(0);
  });

  it("should emit position payload sections when emitSections is true", () => {
    const data = "CALL>APRS:!4903.50N/07201.75W-Test message";
    const frame = Frame.fromString(data);
    const result = frame.decode(true) as {
      payload: PositionPayload;
      structure: Dissected;
    };

    expect(result.payload).not.toBeNull();
    expect(result.payload?.type).toBe(DataType.PositionNoTimestampNoMessaging);

    expect(result.structure).toBeDefined();
    expect(result.structure?.length).toBeGreaterThan(0);

    const positionSection = result.structure?.find((s) => s.name === "position");
    expect(positionSection).toBeDefined();
    expect(positionSection?.data?.byteLength).toBe(19);
    expect(positionSection?.fields).toBeDefined();
    expect(positionSection?.fields?.length).toBeGreaterThan(0);
  });

  it("should not emit sections when emitSections is false or omitted", () => {
    const data = "CALL>APRS:!4903.50N/07201.75W-Test";
    const frame = Frame.fromString(data);
    const result = frame.decode() as PositionPayload;

    expect(result).not.toBeNull();
    expect(result?.type).toBe(DataType.PositionNoTimestampNoMessaging);
    // eslint-disable-next-line  @typescript-eslint/no-explicit-any
    expect((result as any).sections).toBeUndefined();
  });

  it("should emit timestamp section when present", () => {
    const data = "CALL>APRS:@092345z4903.50N/07201.75W>";
    const frame = Frame.fromString(data);
    const result = frame.decode(true) as {
      payload: PositionPayload;
      structure: Dissected;
    };

    expect(result.payload?.type).toBe(DataType.PositionWithTimestampWithMessaging);

    const timestampSection = result.structure?.find((s) => s.name === "timestamp");
    expect(timestampSection).toBeDefined();
    expect(timestampSection?.data?.byteLength).toBe(7);
    expect(timestampSection?.fields?.map((a) => a.name)).toEqual([
      "day (DD)",
      "hour (HH)",
      "minute (MM)",
      "timezone indicator"
    ]);
  });

  it("should emit compressed position sections", () => {
    const data = 'NOCALL-1>APRS:@092345z/:*E";qZ=OMRC/A=088132Hello World!';
    const frame = Frame.fromString(data);
    const result = frame.decode(true) as {
      payload: PositionPayload;
      structure: Dissected;
    };

    expect(result.payload?.type).toBe(DataType.PositionWithTimestampWithMessaging);

    const positionSection = result.structure?.find((s) => s.name === "position");
    expect(positionSection).toBeDefined();
    expect(positionSection?.data?.byteLength).toBe(13);

    const latAttr = positionSection?.fields?.find((a) => a.name === "latitude");
    expect(latAttr).toBeDefined();
    expect(latAttr?.type).toBe(FieldType.STRING);
  });

  it("should emit comment section", () => {
    const data = "CALL>APRS:!4903.50N/07201.75W-Test message";
    const frame = Frame.fromString(data);
    const result = frame.decode(true) as {
      payload: PositionPayload;
      structure: Dissected;
    };

    expect(result.payload?.type).toBe(DataType.PositionNoTimestampNoMessaging);
    const commentSection = result.structure?.find((s) => s.name === "comment");
    expect(commentSection).toBeDefined();
    expect(commentSection?.data?.byteLength).toBe("Test message".length);
    expect(commentSection?.fields?.[0]?.name).toBe("comment");
  });
});

describe("Frame.decodeMessage", () => {
  it("decodes a standard APRS message with 9-char recipient field", () => {
    const raw = "W1AW>APRS::KB1ABC-5 :Hello World";
    const frame = Frame.fromString(raw);
    const decoded = frame.decode() as MessagePayload;

    expect(decoded).not.toBeNull();
    expect(decoded?.type).toBe(DataType.Message);
    expect(decoded?.addressee).toBe("KB1ABC-5");
    expect(decoded?.text).toBe("Hello World");
  });

  it("emits recipient and text sections when emitSections is true", () => {
    const raw = "W1AW>APRS::KB1ABC  :Test via spec";
    const frame = Frame.fromString(raw);
    const res = frame.decode(true) as {
      payload: MessagePayload;
      structure: Dissected;
    };

    expect(res.payload).not.toBeNull();
    expect(res.payload.type).toBe(DataType.Message);
    const recipientSection = res.structure.find((s) => s.name === "recipient");
    const textSection = res.structure.find((s) => s.name === "text");
    expect(recipientSection).toBeDefined();
    expect(textSection).toBeDefined();
    expect(new TextDecoder().decode(recipientSection!.data).trim()).toBe("KB1ABC");
    expect(new TextDecoder().decode(textSection!.data)).toBe("Test via spec");
  });
});

describe("Frame.decoding: object and status", () => {
  it("decodes an object payload (uncompressed position + comment)", () => {
    const raw = "N0CALL>APRS:;OBJECT001*120912z4903.50N/07201.75W>Test object";
    const frame = Frame.parse(raw);

    const res = frame.decode(true) as {
      payload: Payload | null;
      structure: Dissected;
    };
    expect(res).toHaveProperty("payload");
    expect(res.payload).not.toBeNull();

    if (res.payload?.type !== DataType.Object) throw new Error("expected object payload");

    const payload = res.payload as ObjectPayload & { timestamp?: ITimestamp };

    expect(payload.name).toBe("OBJECT001");
    expect(payload.alive).toBe(true);

    expect(payload.timestamp).toBeDefined();
    expect(payload.timestamp?.day).toBe(12);
    expect(payload.timestamp?.hours).toBe(9);
    expect(payload.timestamp?.minutes).toBe(12);

    expect(payload.position).toBeDefined();
    expect(payload.position.latitude).toBeCloseTo(49.058333, 4);
    expect(payload.position.longitude).toBeCloseTo(-72.029166, 4);

    expect(payload.position.comment).toBe("Test object");
  });

  it("decodes a status payload with timestamp and Maidenhead", () => {
    const raw = "N0CALL>APRS:>120912zTesting status FN20";
    const frame = Frame.parse(raw);

    const res = frame.decode(true) as {
      payload: Payload | null;
      structure: Dissected;
    };
    expect(res).toHaveProperty("payload");
    expect(res.payload).not.toBeNull();

    if (res.payload?.type !== DataType.Status) throw new Error("expected status payload");

    const payload = res.payload as StatusPayload & { timestamp?: ITimestamp };

    expect(payload.type).toBe(DataType.Status);

    expect(payload.timestamp).toBeDefined();
    expect(payload.timestamp?.day).toBe(12);

    expect(payload.text).toBe("Testing status");
    expect(payload.maidenhead).toBe("FN20");
  });
});
