import { describe, expect, it } from 'vitest';
import { Address, Frame, Timestamp } from '../src/frame';
import { Payload, PositionPayload } from '../src/frame.types';
import { FieldType, PacketSegment, PacketStructure } from '../src/parser.types';

describe('parseAddress', () => {
  it('should parse callsign without SSID', () => {
    const result = Address.parse('NOCALL');
    expect(result).toEqual({
      call: 'NOCALL',
      ssid: '',
      isRepeated: false,
    });
  });

  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,
    });
  });
});

describe('Frame.fromString', () => {
  it('should parse 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('should parse 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('should parse 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).toEqual({
      call: 'KB1ABC',
      ssid: '5',
      isRepeated: false,
    });

    expect(result.destination).toEqual({
      call: 'APRS',
      ssid: '',
      isRepeated: false,
    });

    expect(result.path).toHaveLength(3);
    expect(result.path[0]).toEqual({
      call: 'WIDE1',
      ssid: '1',
      isRepeated: false,
    });
    expect(result.path[1]).toEqual({
      call: 'WIDE2',
      ssid: '2',
      isRepeated: true,
    });
    expect(result.path[2]).toEqual({
      call: 'IGATE',
      ssid: '',
      isRepeated: false,
    });

    expect(result.payload).toBe('!4903.50N/07201.75W-Test');
  });

  it('should parse frame with no path', () => {
    const data = 'W1AW>APRS::STATUS:Testing';
    const result = Frame.fromString(data);

    expect(result.source).toEqual({
      call: 'W1AW',
      ssid: '',
      isRepeated: false,
    });

    expect(result.destination).toEqual({
      call: 'APRS',
      ssid: '',
      isRepeated: false,
    });

    expect(result.path).toHaveLength(0);
    expect(result.payload).toBe(':STATUS:Testing');
  });

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

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

describe('Frame class', () => {
  it('should return a Frame instance from Frame.fromString', () => {
    const data = 'W1AW>APRS:>Status message';
    const result = Frame.fromString(data);

    expect(result).toBeInstanceOf(Object);
    expect(typeof result.decode).toBe('function');
    expect(typeof result.getDataTypeIdentifier).toBe('function');
  });

  it('should get correct data type identifier for position', () => {
    const data = 'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!';
    const frame = Frame.fromString(data);

    expect(frame.getDataTypeIdentifier()).toBe('@');
  });

  it('should get correct data type identifier for Mic-E', () => {
    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('should get correct data type identifier for message', () => {
    const data = 'W1AW>APRS::KB1ABC-5 :Hello World';
    const frame = Frame.fromString(data);

    expect(frame.getDataTypeIdentifier()).toBe(':');
  });

  it('should get correct data type identifier for status', () => {
    const data = 'W1AW>APRS:>Status message';
    const frame = Frame.fromString(data);

    expect(frame.getDataTypeIdentifier()).toBe('>');
  });

it('should call decode method and return position data', () => {
      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;

      // The test vector actually decodes to position data now that we've implemented it
      expect(decoded).not.toBeNull();
      expect(decoded?.type).toBe('position');
  });

  it('should handle various data type identifiers in decode', () => {
    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);
      // decode() returns null for now since implementations are TODO
      expect(() => frame.decode()).not.toThrow();
    }
  });

  describe('Position decoding', () => {
    it('should decode position with timestamp and compressed format (test vector 1)', () => {
      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('position');

      if (decoded && decoded.type === 'position') {
        expect(decoded.messaging).toBe(true);
        expect(decoded.timestamp).toBeDefined();
        expect(decoded.timestamp?.day).toBe(9);
        expect(decoded.timestamp?.hours).toBe(23);
        expect(decoded.timestamp?.minutes).toBe(45);
        expect(decoded.timestamp?.format).toBe('DHM');
        expect(decoded.timestamp?.zulu).toBe(true);

        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.position.altitude).toBeCloseTo(88132 * 0.3048, 1); // feet to meters
        expect(decoded.position.comment).toContain('Hello World!');
      }
    });

    it('should decode uncompressed position without timestamp', () => {
      const data = 'KB1ABC>APRS:!4903.50N/07201.75W-Test message';
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();
      expect(decoded?.type).toBe('position');

      if (decoded && decoded.type === 'position') {
        expect(decoded.messaging).toBe(false);
        expect(decoded.timestamp).toBeUndefined();

        // 49 degrees + 3.50 minutes = 49.0583
        expect(decoded.position.latitude).toBeCloseTo(49 + 3.50/60, 3);
        // -72 degrees - 1.75 minutes = -72.0292
        expect(decoded.position.longitude).toBeCloseTo(-(72 + 1.75/60), 3);
        expect(decoded.position.symbol?.table).toBe('/');
        expect(decoded.position.symbol?.code).toBe('-');
        expect(decoded.position.comment).toBe('Test message');
      }
    });

      it('should decode uncompressed position with alternate symbol table (\\)', () => {
        const data = String.raw`W1AW>APRS:!4903.50N\07201.75W-Test message`;
        const frame = Frame.fromString(data);
        const decoded = frame.decode() as Payload;

        expect(decoded).not.toBeNull();
        expect(decoded?.type).toBe('position');

        if (decoded && decoded.type === 'position') {
          expect(decoded.position.symbol?.table).toBe('\\');
          expect(decoded.position.comment).toBe('Test message');
        }
      });
    it('should decode position with messaging capability', () => {
      const data = 'W1AW>APRS:=4903.50N/07201.75W-';
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();
      expect(decoded?.type).toBe('position');

      if (decoded && decoded.type === 'position') {
        expect(decoded.messaging).toBe(true);
        expect(decoded.timestamp).toBeUndefined();
      }
    });

    it('should decode position with timestamp (HMS format)', () => {
      const data = 'CALL>APRS:/234517h4903.50N/07201.75W>';
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();
      expect(decoded?.type).toBe('position');

      if (decoded && decoded.type === 'position') {
        expect(decoded.messaging).toBe(false);
        expect(decoded.timestamp).toBeDefined();
        expect(decoded.timestamp?.hours).toBe(23);
        expect(decoded.timestamp?.minutes).toBe(45);
        expect(decoded.timestamp?.seconds).toBe(17);
        expect(decoded.timestamp?.format).toBe('HMS');
      }
    });

    it('should extract altitude from comment', () => {
      const data = 'CALL>APRS:!4903.50N/07201.75W>/A=001234';
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === 'position') {
        expect(decoded.position.altitude).toBeCloseTo(1234 * 0.3048, 1);
      }
    });

    it('should handle southern and western hemispheres', () => {
      const data = 'CALL>APRS:!3345.67S/15112.34E-';
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === 'position') {
        // 33 degrees + 45.67 minutes = 33.7612 degrees South = -33.7612
        expect(decoded.position.latitude).toBeLessThan(0);
        expect(decoded.position.longitude).toBeGreaterThan(0);
        expect(decoded.position.latitude).toBeCloseTo(-(33 + 45.67/60), 3);
        // 151 degrees + 12.34 minutes = 151.2057 degrees East
        expect(decoded.position.longitude).toBeCloseTo(151 + 12.34/60, 3);
      }
    });

    it('should return null for invalid position data', () => {
      const data = 'CALL>APRS:!invalid';
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

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

    it('should handle position with ambiguity level 1 (1 space)', () => {
      // Spaces mask the rightmost digits for privacy
      // 4903.5  means ambiguity level 1 (±0.05 minute)
      const data = 'CALL>APRS:!4903.5 N/07201.75W-';
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === 'position') {
        expect(decoded.position.ambiguity).toBe(1);
        // Spaces are replaced with 0 for parsing, so 4903.5  becomes 4903.50
        expect(decoded.position.latitude).toBeCloseTo(49 + 3.5/60, 3);
      }
    });

    it('should handle position with ambiguity level 2 (2 spaces)', () => {
      // 4903.  means ambiguity level 2 (±0.5 minutes) - spaces replace last 2 decimal digits
      const data = 'CALL>APRS:!4903.  N/07201.75W-';
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === 'position') {
        expect(decoded.position.ambiguity).toBe(2);
        expect(decoded.position.latitude).toBeCloseTo(49 + 3/60, 3);
      }
    });

    it('should handle position with ambiguity level 1 in both lat and lon', () => {
      // Both lat and lon have 1 space for ambiguity 1 (±0.05 minute)
      const data = 'CALL>APRS:!4903.5 N/07201.7 W-';
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === 'position') {
        expect(decoded.position.ambiguity).toBe(1);
        expect(decoded.position.latitude).toBeCloseTo(49 + 3.5/60, 3);
        expect(decoded.position.longitude).toBeCloseTo(-(72 + 1.7/60), 3);
      }
    });

    it('should handle position with ambiguity level 2 in both lat and lon', () => {
      // Both lat and lon have 2 spaces for ambiguity 2 (±0.5 minutes)
      const data = 'CALL>APRS:!4903.  N/07201.  W-';
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === 'position') {
        expect(decoded.position.ambiguity).toBe(2);
        expect(decoded.position.latitude).toBeCloseTo(49 + 3/60, 3);
        expect(decoded.position.longitude).toBeCloseTo(-(72 + 1/60), 3);
      }
    });

    it('should have toDate method on timestamp', () => {
      const data = 'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!';
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === 'position' && decoded.timestamp) {
        expect(typeof decoded.timestamp.toDate).toBe('function');
        const date = decoded.timestamp.toDate();
        expect(date).toBeInstanceOf(Date);
        expect(date.getUTCDate()).toBe(9);
        expect(date.getUTCHours()).toBe(23);
        expect(date.getUTCMinutes()).toBe(45);
      }
    });
  });

  describe('Object decoding', () => {
    it('should decode 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 Payload;

      expect(decoded).not.toBeNull();
      expect(decoded?.type).toBe('object');

      if (decoded && decoded.type === 'object') {
        expect(decoded.name).toBe('OBJECT');
        expect(decoded.alive).toBe(true);
        expect(decoded.timestamp).toBeDefined();
        expect(decoded.position).toBeDefined();
        expect(typeof decoded.position.latitude).toBe('number');
        expect(typeof decoded.position.longitude).toBe('number');
        expect(decoded.position.comment).toBe('Test object');
      }
    });

    it('should emit 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: Payload | null; structure: PacketStructure };

      expect(result.payload).not.toBeNull();
      expect(result.payload?.type).toBe('object');
      expect(result.structure.length).toBeGreaterThan(0);

      const nameSection = result.structure.find((s) => s.name === 'object name');
      expect(nameSection).toBeDefined();
      const stateSection = result.structure.find((s) => s.name === 'object state');
      expect(stateSection).toBeDefined();
      const timestampSection = result.structure.find((s) => s.name === 'timestamp');
      expect(timestampSection).toBeDefined();
      const positionSection = result.structure.find((s) => s.name === 'position');
      expect(positionSection).toBeDefined();
    });
  });
});

describe('Timestamp class', () => {
  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);
    }
  });
});

describe('Mic-E decoding', () => {
  describe('Basic Mic-E frames', () => {
    it('should decode a basic Mic-E packet (current format)', () => {
      // Destination: T2TQ5U encodes latitude ~42.3456N
      // T=1, 2=2, T=1, Q=1, 5=5, U=1 (digits for latitude)
      // Information field encodes longitude, speed, course, and symbols
      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 Payload;

      expect(decoded).not.toBeNull();
      expect(decoded?.type).toBe('position');

      if (decoded && decoded.type === 'position') {
        expect(decoded.messaging).toBe(true);
        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.micE).toBeDefined();
        expect(decoded.micE?.messageType).toBeDefined();
      }
    });

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

      expect(decoded).not.toBeNull();
      expect(decoded?.type).toBe('position');
    });
  });

  describe('Latitude decoding from destination', () => {
    it('should decode latitude from numeric digits (0-9)', () => {
      // Destination: 123456 -> 12°34.56'N with specific message bits
      const data = 'CALL>123456:`c.l+@&\'/\'"G:}';
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === 'position') {
        // 12 degrees + 34.56 minutes = 12.576 degrees
        expect(decoded.position.latitude).toBeCloseTo(12 + 34.56/60, 3);
      }
    });

    it('should decode latitude from letter digits (A-J)', () => {
      // A=0, B=1, C=2, D=3, E=4, F=5, G=6, H=7, I=8, J=9
      // ABC0EF -> 012045 -> 01°20.45' (using 0 at position 3 for North)
      const data = 'CALL>ABC0EF:`c.l+@&\'/\'"G:}';
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === 'position') {
        // 01 degrees + 20.45 minutes = 1.340833 degrees North
        expect(decoded.position.latitude).toBeCloseTo(1 + 20.45/60, 3);
      }
    });

    it('should decode latitude with mixed digits and letters', () => {
      // 4AB2DE -> 401234 -> 40°12.34'N (using 2 at position 3 for North)
      const data = 'CALL>4AB2DE:`c.l+@&\'/\'"G:}';
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();

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

    it('should decode latitude for southern hemisphere', () => {
      // When messageBits[3] == 1, it's southern hemisphere
      // For 'P' char at position 3 (msgBit=1 for south), combined appropriately
      const data = 'CALL>4A0P0U:`c.l+@&\'/\'"G:}';
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === 'position') {
        // Should be negative for south
        expect(decoded.position.latitude).toBeLessThan(0);
      }
    });
  });

  describe('Longitude decoding from information field', () => {
    it('should decode longitude from information field', () => {
      // Mic-E info field bytes encode longitude degrees, minutes, hundredths
      // Testing with constructed values
      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 === 'position') {
        expect(typeof decoded.position.longitude).toBe('number');
        // Longitude should be within valid range
        expect(decoded.position.longitude).toBeGreaterThanOrEqual(-180);
        expect(decoded.position.longitude).toBeLessThanOrEqual(180);
      }
    });

    it('should handle eastern hemisphere longitude', () => {
      // When messageBits[4] == 1, it's eastern hemisphere (positive)
      // Need specific destination encoding for this
      const data = 'CALL>4ABPDE:`c.l+@&\'/\'"G:}';
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === 'position') {
        // Eastern hemisphere could be positive (depends on other flags)
        expect(typeof decoded.position.longitude).toBe('number');
      }
    });

    it('should handle longitude offset +100', () => {
      // When messageBits[5] == 1, add 100 to longitude degrees
      // Need 'P' or similar at position 5
      const data = 'CALL>4ABCDP:`c.l+@&\'/\'"G:}';
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === 'position') {
        expect(typeof decoded.position.longitude).toBe('number');
        // Longitude offset should be applied (100+ degrees)
        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 === 'position') {
        // Speed might be 0 or present
        if (decoded.position.speed !== undefined) {
          expect(decoded.position.speed).toBeGreaterThanOrEqual(0);
          // Speed should be in km/h
          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 === 'position') {
        // Course might be 0 or present
        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', () => {
      // When speed is 0, it should not be included in the 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 === 'position') {
        // Speed of 0 should not be set
        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 Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === 'position') {
        // Course of 0 or >= 360 should not be set
        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 === 'position') {
        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+@&\'/\'"G:}/A=001234';
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === 'position') {
        // 1234 feet = 1234 * 0.3048 meters
        expect(decoded.position.altitude).toBeCloseTo(1234 * 0.3048, 1);
      }
    });

    it('should decode altitude from base-91 format }abc', () => {
      // Base-91 altitude format: }xyz where xyz is base-91 encoded altitude
      // The altitude encoding is (altitude in feet + 10000) in base-91
      const data = 'CALL>4AB2DE:`c.l+@&\'/\'"G:}}S^X';
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === 'position') {
        // Base-91 altitude should be decoded (using valid base-91 characters)
        // Even if altitude calculation results in negative value, it should be defined
        if (decoded.position.comment?.startsWith('}')) {
          // Altitude should be extracted from base-91 format
          expect(decoded.position.altitude).toBeDefined();
        }
      }
    });

    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 Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === 'position') {
        // Should use /A= format (5000 feet)
        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 Payload;

      expect(decoded).not.toBeNull();

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

  describe('Message type decoding', () => {
    it('should decode message type M0 (Off Duty)', () => {
      // Message bits 0,1,2 = 0,0,0 -> M0: Off Duty
      // Use digits 0-9 for message bit 0
      const data = 'CALL>012345:`c.l+@&\'/\'"G:}';
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === 'position') {
        expect(decoded.micE?.messageType).toBe('M0: Off Duty');
      }
    });

    it('should decode message type M7 (Emergency)', () => {
      // Message bits 0,1,2 = 1,1,1 -> M7: Emergency
      // Use letters A-J for message bit 1
      const data = 'CALL>ABCDEF:`c.l+@&\'/\'"G:}';
      const frame = Frame.fromString(data);
      const decoded = frame.decode() as Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === 'position') {
        // Should contain a message type
        expect(decoded.micE?.messageType).toBeDefined();
        expect(typeof decoded.micE?.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 Payload;

      expect(decoded).not.toBeNull();

      if (decoded && decoded.type === 'position') {
        expect(decoded.micE?.isStandard).toBeDefined();
        expect(typeof decoded.micE?.isStandard).toBe('boolean');
      }
    });
  });

  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 === 'position') {
        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 === 'position') {
        // Empty comment should still be defined but empty
        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;

      // Should fail because destination is too short
      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;

      // Should fail because payload is too short (needs at least 9 bytes with data type)
      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;

      // Should fail because destination contains invalid characters
      expect(decoded).toBeNull();
    });

    it('should handle exceptions gracefully', () => {
      // Malformed data that might cause exceptions
      const data = 'CALL>4ABCDE:`\x00\x00\x00\x00\x00\x00\x00\x00';
      const frame = Frame.fromString(data);

      // Should not throw, just return null
      expect(() => frame.decode()).not.toThrow();
      const decoded = frame.decode() as Payload;
      // Might be null or might decode with weird values
      expect(decoded === null || decoded?.type === 'position').toBe(true);
    });
  });

  describe('Real-world test vectors', () => {
    it('should decode real Mic-E packet from test vector 2', () => {
      // From the existing test: N83MZ>T2TQ5U with specific encoding
      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 Payload;

      expect(decoded).not.toBeNull();
      expect(decoded?.type).toBe('position');

      if (decoded && decoded.type === 'position') {
        expect(decoded.messaging).toBe(true);
        expect(decoded.position.latitude).toBeDefined();
        expect(decoded.position.longitude).toBeDefined();
        expect(decoded.position.symbol).toBeDefined();
        expect(decoded.micE).toBeDefined();

        // Verify reasonable coordinate ranges
        expect(Math.abs(decoded.position.latitude)).toBeLessThanOrEqual(90);
        expect(Math.abs(decoded.position.longitude)).toBeLessThanOrEqual(180);
      }
    });
  });

  describe('Messaging capability', () => {
    it('should always set messaging to true for Mic-E', () => {
      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 === 'position') {
        // Mic-E always has messaging capability
        expect(decoded.messaging).toBe(true);
      }
    });
  });
});

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: PacketStructure };

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

    // Check routing section (capital R as implemented)
    const routingSection = result.structure.find(s => s.name === 'Routing');
    expect(routingSection).toBeDefined();
    expect(routingSection?.fields).toBeDefined();
    expect(routingSection?.fields?.length).toBeGreaterThan(0);

    // Check for source and destination fields
    const sourceField = routingSection?.fields?.find(a => a.name === 'Source address');
    expect(sourceField).toBeDefined();
    expect(sourceField?.size).toBeGreaterThan(0);

    const destField = routingSection?.fields?.find(a => a.name === 'Destination address');
    expect(destField).toBeDefined();
    expect(destField?.size).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: Payload; structure: PacketStructure };

    expect(result.payload).not.toBeNull();
    expect(result.payload?.type).toBe('position');

    // Check if result has sections at top level
    expect(result.structure).toBeDefined();
    expect(result.structure?.length).toBeGreaterThan(0);

    // Find position section
    const positionSection = result.structure?.find(s => s.name === 'position');
    expect(positionSection).toBeDefined();
    expect(positionSection?.data.length).toBe(19); // Uncompressed position is 19 bytes
    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 Payload;

    // Result should be just the DecodedPayload, not an object with payload and sections
    expect(result).not.toBeNull();
    expect(result?.type).toBe('position');
    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: Payload; structure: PacketStructure };

    expect(result.payload?.type).toBe('position');

    const timestampSection = result.structure?.find(s => s.name === 'timestamp');
    expect(timestampSection).toBeDefined();
    expect(timestampSection?.data.length).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: Payload; structure: PacketStructure };

    expect(result.payload?.type).toBe('position');

    const positionSection = result.structure?.find(s => s.name === 'position');
    expect(positionSection).toBeDefined();
    expect(positionSection?.data.length).toBe(13); // Compressed position is 13 bytes

    // Check for base91 encoded attributes
    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: Payload; structure: PacketStructure };

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