import { describe, expect, it } from 'vitest';
import { bytesToHex } from '@noble/hashes/utils.js';

import { GroupSecret, Packet } from './meshcore';
import type {
  AdvertPayload,
  AnonReqPayload,
  ControlPayload,
  GroupDataPayload,
  GroupTextPayload,
  MultipartPayload,
  PathPayload,
  RawCustomPayload,
  RequestPayload,
  ResponsePayload,
  TextPayload,
  TracePayload,
} from './meshcore.types';
import { AdvertisementFlags, PayloadType, RouteType } from './meshcore.types';

describe('GroupSecret', () => {
  describe('fromName', () => {
    it('should generate correct hash for "#test"', () => {
      const secret = GroupSecret.fromName('#test');
      const bytes = secret.toBytes();

      // Test vector: '#test' should produce '9cd8fcf22a47333b591d96a2b848b73f' (first 16 bytes)
      const hex = bytesToHex(bytes.slice(0, 16));
      expect(hex).toBe('9cd8fcf22a47333b591d96a2b848b73f');
    });

    it('should generate a 32-byte key', () => {
      const secret = GroupSecret.fromName('#test');
      const bytes = secret.toBytes();

      expect(bytes.length).toBe(32);
    });

    it('should generate consistent hashes for same input', () => {
      const secret1 = GroupSecret.fromName('mygroup');
      const secret2 = GroupSecret.fromName('mygroup');

      expect(secret1.toString()).toBe(secret2.toString());
    });

    it('should generate different hashes for different inputs', () => {
      const secret1 = GroupSecret.fromName('group1');
      const secret2 = GroupSecret.fromName('group2');

      expect(secret1.toString()).not.toBe(secret2.toString());
    });

    it('should generate valid channel hash', () => {
      const secret = GroupSecret.fromName('#test');
      const hash = secret.toHash();

      // Channel hash should be 2 hex characters
      expect(hash).toMatch(/^[0-9a-f]{2}$/);
    });
  });

  describe('constructor', () => {
    it('should accept 32-byte hex string', () => {
      const hexKey = '9cd8fcf22a47333b591d96a2b848b73f9cd8fcf22a47333b591d96a2b848b73f';
      const secret = new GroupSecret(hexKey);

      expect(secret.toString()).toBe(hexKey);
    });

    it('should accept 32-byte Uint8Array', () => {
      const bytes = new Uint8Array(32).fill(0x42);
      const secret = new GroupSecret(bytes);

      expect(secret.toBytes()).toEqual(bytes);
    });

    it('should accept and pad 16-byte hex string', () => {
      const hexKey = '9cd8fcf22a47333b591d96a2b848b73f';
      const secret = new GroupSecret(hexKey);
      const bytes = secret.toBytes();

      expect(bytes.length).toBe(32);
      // First 16 bytes should be the input
      expect(bytesToHex(bytes.slice(0, 16))).toBe(hexKey);
      // Last 16 bytes should be zeros
      expect(bytes.slice(16, 32)).toEqual(new Uint8Array(16));
    });

    it('should accept and pad 16-byte Uint8Array', () => {
      const input = new Uint8Array(16).fill(0x42);
      const secret = new GroupSecret(input);
      const bytes = secret.toBytes();

      expect(bytes.length).toBe(32);
      // First 16 bytes should be the input
      expect(bytes.slice(0, 16)).toEqual(input);
      // Last 16 bytes should be zeros
      expect(bytes.slice(16, 32)).toEqual(new Uint8Array(16));
    });

    it('should throw error for invalid key length', () => {
      expect(() => new GroupSecret('abcd')).toThrow('invalid group secret');
    });
  });

  describe('toHash', () => {
    it('should return first byte as 2-char hex string', () => {
      const bytes = new Uint8Array(32);
      bytes[0] = 0x9c; // First byte from test vector
      Object.defineProperty(bytes, 'length', { value: 32 }); // Ensure length is 32

      for (let i = 1; i < 16; i++) {
        bytes[i] = 0;
      }
      bytes.set(new Uint8Array([
        0x9c, 0xd8, 0xfc, 0xf2, 0x2a, 0x47, 0x33, 0x3b,
        0x59, 0x1d, 0x96, 0xa2, 0xb8, 0x48, 0xb7, 0x3f
      ]), 16);

      const secret = new GroupSecret(bytes);
      expect(secret.toHash()).toBe('9c');
    });

    it('should pad single-digit hex with zero', () => {
      const bytes = new Uint8Array(32);
      bytes[0] = 0x05; // Should become '05', not '5'

      const secret = new GroupSecret(bytes);
      expect(secret.toHash()).toBe('05');
    });
  });

  describe('specific group test vectors', () => {
    describe('Public group', () => {
      const publicKey = '8b3387e9c5cdea6ac9e5edbaa115cd72';

      it('should create GroupSecret with 16-byte key for Public group', () => {
        const secret = new GroupSecret(publicKey);
        const bytes = secret.toBytes();

        expect(bytes.length).toBe(32);
        // First 16 bytes should match the input key
        expect(bytesToHex(bytes.slice(0, 16))).toBe(publicKey);
      });

      it('should pad Public group key with 16 trailing zero bytes', () => {
        const secret = new GroupSecret(publicKey);
        const bytes = secret.toBytes();

        // Last 16 bytes should be zeros
        expect(bytes.slice(16, 32)).toEqual(new Uint8Array(16));
      });

      it('should generate correct channel hash for Public group', () => {
        const secret = new GroupSecret(publicKey);
        const hash = secret.toHash();

        // First byte of the key (0x8b) should be '8b'
        expect(hash).toBe('8b');
      });

      it('should NOT match key derived from name "Public"', () => {
        const secretFromKey = new GroupSecret(publicKey);
        const secretFromName = GroupSecret.fromName('Public');

        // These should be different since the key can't be derived from the name
        expect(secretFromKey.toString()).not.toBe(secretFromName.toString());
      });

      it('should have different channel hash than fromName("Public")', () => {
        const secretFromKey = new GroupSecret(publicKey);
        const secretFromName = GroupSecret.fromName('Public');

        expect(secretFromKey.toHash()).not.toBe(secretFromName.toHash());
      });
    });
  });
});

describe('Packet', () => {
  // Helper function to create a minimal valid packet
  const createPacketData = (routeType: number, payloadType: number, payloadData: Uint8Array): Uint8Array => {
    const header = (routeType & 0x03) | ((payloadType & 0x0F) << 2) | (0x01 << 6); // version = 1
    const pathLength = 0; // No path for simplicity

    // Check if this route type needs transport codes
    const needsTransportCodes = routeType === RouteType.TRANSPORT_FLOOD || routeType === RouteType.TRANSPORT_DIRECT;

    if (needsTransportCodes) {
      // Add transport codes (4 bytes: two uint16 LE)
      const transportCodes = new Uint8Array([0x00, 0x00, 0x00, 0x00]);
      return new Uint8Array([header, ...transportCodes, pathLength, ...payloadData]);
    }

    return new Uint8Array([header, pathLength, ...payloadData]);
  };

  describe('constructor and parsing', () => {
    it('should create empty packet with no data', () => {
      const packet = new Packet();
      expect(packet).toBeInstanceOf(Packet);
    });

    it('should parse packet from Uint8Array', () => {
      const payloadData = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
      const packetData = createPacketData(RouteType.FLOOD, PayloadType.TRACE, payloadData);

      const packet = new Packet(packetData);
      expect(packet.decode()).toBeDefined();
    });

    it('should parse packet from base64 string', () => {
      const payloadData = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
      const packetData = createPacketData(RouteType.FLOOD, PayloadType.TRACE, payloadData);
      // Convert to base64
      const base64 = btoa(String.fromCharCode(...packetData));

      const packet = new Packet(base64);
      expect(packet.decode()).toBeDefined();
    });

    it('should throw error for packet without payload', () => {
      const data = new Uint8Array([0x04, 0x00]); // header + pathLength, no payload
      expect(() => new Packet(data)).toThrow();
    });

    it('should throw error for invalid path length', () => {
      // pathLength with hashSize=4 (reserved) and hashCount=1
      const data = new Uint8Array([0x04, 0xC1, 0x00]); // header, invalid pathLength, dummy payload
      expect(() => new Packet(data)).toThrow();
    });
  });

  describe('decode - REQUEST payload', () => {
    it('should decode REQUEST payload', () => {
      // REQUEST: dstHash(1) + srcHash(1) + cipherMAC(2) + cipherText(n)
      const payloadData = new Uint8Array([
        0xAB, // dstHash
        0xCD, // srcHash
        0x12, 0x34, // cipherMAC (LE)
        0x01, 0x02, 0x03, 0x04 // cipherText
      ]);
      const packetData = createPacketData(RouteType.FLOOD, PayloadType.REQUEST, payloadData);

      const packet = new Packet(packetData);
      const decoded = packet.decode() as RequestPayload;

      expect(decoded.payloadType).toBe(PayloadType.REQUEST);
      expect(decoded.dstHash).toBe('ab');
      expect(decoded.srcHash).toBe('cd');
      expect(decoded.cipherMAC).toEqual(new Uint8Array([0x12, 0x34]));
      expect(decoded.cipherText).toEqual(new Uint8Array([0x01, 0x02, 0x03, 0x04]));
    });
  });

  describe('decode - RESPONSE payload', () => {
    it('should decode RESPONSE payload', () => {
      const payloadData = new Uint8Array([
        0x11, // dstHash
        0x22, // srcHash
        0xAB, 0xCD, // cipherMAC (LE)
        0x05, 0x06, 0x07 // cipherText
      ]);
      const packetData = createPacketData(RouteType.FLOOD, PayloadType.RESPONSE, payloadData);

      const packet = new Packet(packetData);
      const decoded = packet.decode() as ResponsePayload;

      expect(decoded.payloadType).toBe(PayloadType.RESPONSE);
      expect(decoded.dstHash).toBe('11');
      expect(decoded.srcHash).toBe('22');
    });
  });

  describe('decode - TEXT payload', () => {
    it('should decode TEXT payload', () => {
      const payloadData = new Uint8Array([
        0x33, // dstHash
        0x44, // srcHash
        0x11, 0x22, // cipherMAC (LE)
        0x48, 0x65, 0x6C, 0x6C, 0x6F // "Hello" encrypted
      ]);
      const packetData = createPacketData(RouteType.FLOOD, PayloadType.TEXT, payloadData);

      const packet = new Packet(packetData);
      const decoded = packet.decode() as TextPayload;

      expect(decoded.payloadType).toBe(PayloadType.TEXT);
      expect(decoded.dstHash).toBe('33');
      expect(decoded.srcHash).toBe('44');
    });
  });

  describe('decode - ACK payload', () => {
    it('should decode ACK payload', () => {
      const payloadData = new Uint8Array([
        0x55, // dstHash
        0x66, // srcHash
        0xFF, 0xFF, // cipherMAC (LE)
        0xAA, 0xBB // cipherText
      ]);
      const packetData = createPacketData(RouteType.FLOOD, PayloadType.ACK, payloadData);

      const packet = new Packet(packetData);
      // Note: ACK uses the same encrypted structure as REQUEST/RESPONSE/TEXT
      const decoded = packet.decode() as RequestPayload;

      expect(decoded.payloadType).toBe(PayloadType.ACK);
      expect(decoded.dstHash).toBe('55');
      expect(decoded.srcHash).toBe('66');
    });
  });

  describe('decode - ADVERT payload', () => {
    it('should decode ADVERT payload with no optional fields', () => {
      const publicKey = new Uint8Array(32).fill(0x42);
      const signature = new Uint8Array(64).fill(0xAA);
      const timestamp = 1234567890; // Unix timestamp in seconds
      const flags = 0x00; // No optional fields

      const payloadData = new Uint8Array([
        ...publicKey,
        timestamp & 0xFF, (timestamp >> 8) & 0xFF, (timestamp >> 16) & 0xFF, (timestamp >> 24) & 0xFF, // timestamp LE
        ...signature,
        flags
      ]);
      const packetData = createPacketData(RouteType.FLOOD, PayloadType.ADVERT, payloadData);

      const packet = new Packet(packetData);
      const decoded = packet.decode() as AdvertPayload;

      expect(decoded.payloadType).toBe(PayloadType.ADVERT);
      expect(decoded.publicKey).toEqual(publicKey);
      expect(decoded.signature).toEqual(signature);
      expect(decoded.timestamp?.getTime()).toBe(timestamp);
      expect(decoded.appdata.flags).toBe(flags);
    });

    it('should decode ADVERT payload with location', () => {
      const publicKey = new Uint8Array(32).fill(0x42);
      const signature = new Uint8Array(64).fill(0xAA);
      const timestamp = 1234567890;
      const flags = AdvertisementFlags.HAS_LOCATION; // 0x10
      const latitude = 37500000; // 37.5 degrees
      const longitude = -122400000; // -122.4 degrees

      const payloadData = new Uint8Array([
        ...publicKey,
        timestamp & 0xFF, (timestamp >> 8) & 0xFF, (timestamp >> 16) & 0xFF, (timestamp >> 24) & 0xFF,
        ...signature,
        flags,
        latitude & 0xFF, (latitude >> 8) & 0xFF, (latitude >> 16) & 0xFF, (latitude >> 24) & 0xFF, // lat LE
        longitude & 0xFF, (longitude >> 8) & 0xFF, (longitude >> 16) & 0xFF, (longitude >> 24) & 0xFF // lon LE
      ]);
      const packetData = createPacketData(RouteType.FLOOD, PayloadType.ADVERT, payloadData);

      const packet = new Packet(packetData);
      const decoded = packet.decode() as AdvertPayload;

      expect(decoded.payloadType).toBe(PayloadType.ADVERT);
      expect(decoded.appdata.flags).toBe(flags);
      expect(decoded.appdata.latitude).toBeCloseTo(37.5, 6);
      expect(decoded.appdata.longitude).toBeCloseTo(-122.4, 6);
    });

    it('should decode ADVERT payload with name', () => {
      const publicKey = new Uint8Array(32).fill(0x42);
      const signature = new Uint8Array(64).fill(0xAA);
      const timestamp = 1234567890;
      const flags = AdvertisementFlags.HAS_NAME; // 0x80
      const name = 'TestNode';
      const nameBytes = new TextEncoder().encode(name);

      const payloadData = new Uint8Array([
        ...publicKey,
        timestamp & 0xFF, (timestamp >> 8) & 0xFF, (timestamp >> 16) & 0xFF, (timestamp >> 24) & 0xFF,
        ...signature,
        flags,
        ...nameBytes
      ]);
      const packetData = createPacketData(RouteType.FLOOD, PayloadType.ADVERT, payloadData);

      const packet = new Packet(packetData);
      const decoded = packet.decode() as AdvertPayload;

      expect(decoded.payloadType).toBe(PayloadType.ADVERT);
      expect(decoded.appdata.name).toBe(name);
    });
  });

  describe('decode - GROUP_TEXT payload', () => {
    it('should decode GROUP_TEXT payload', () => {
      const payloadData = new Uint8Array([
        0x9C, // channelHash
        0xAA, 0xBB, // cipherMAC
        0x01, 0x02, 0x03, 0x04 // cipherText
      ]);
      const packetData = createPacketData(RouteType.FLOOD, PayloadType.GROUP_TEXT, payloadData);

      const packet = new Packet(packetData);
      const decoded = packet.decode() as GroupTextPayload;

      expect(decoded.payloadType).toBe(PayloadType.GROUP_TEXT);
      expect(decoded.channelHash).toBe('9c');
      expect(decoded.cipherMAC).toEqual(new Uint8Array([0xAA, 0xBB]));
      expect(decoded.cipherText).toEqual(new Uint8Array([0x01, 0x02, 0x03, 0x04]));
    });
  });

  describe('decode - GROUP_DATA payload', () => {
    it('should decode GROUP_DATA payload', () => {
      const payloadData = new Uint8Array([
        0x8B, // channelHash
        0xFF, 0xEE, // cipherMAC
        0x10, 0x20, 0x30 // cipherText
      ]);
      const packetData = createPacketData(RouteType.FLOOD, PayloadType.GROUP_DATA, payloadData);

      const packet = new Packet(packetData);
      const decoded = packet.decode() as GroupDataPayload;

      expect(decoded.payloadType).toBe(PayloadType.GROUP_DATA);
      expect(decoded.channelHash).toBe('8b');
      expect(decoded.cipherMAC).toEqual(new Uint8Array([0xFF, 0xEE]));
    });
  });

  describe('decode - ANON_REQ payload', () => {
    it('should decode ANON_REQ payload', () => {
      const publicKey = new Uint8Array(32).fill(0x99);
      const payloadData = new Uint8Array([
        0x77, // dstHash
        ...publicKey,
        0x12, 0x34, // cipherMAC
        0xAA, 0xBB, 0xCC // cipherText
      ]);
      const packetData = createPacketData(RouteType.FLOOD, PayloadType.ANON_REQ, payloadData);

      const packet = new Packet(packetData);
      const decoded = packet.decode() as AnonReqPayload;

      expect(decoded.payloadType).toBe(PayloadType.ANON_REQ);
      expect(decoded.dstHash).toBe('77');
      expect(decoded.publicKey).toEqual(publicKey);
      expect(decoded.cipherMAC).toEqual(new Uint8Array([0x12, 0x34]));
      expect(decoded.cipherText).toEqual(new Uint8Array([0xAA, 0xBB, 0xCC]));
    });
  });

  describe('decode - PATH payload', () => {
    it('should decode PATH payload', () => {
      const payloadData = new Uint8Array([
        0x88, // dstHash
        0x99, // srcHash
        0xAA, 0xBB, // cipherMAC (LE)
        0x01, 0x02, 0x03 // cipherText
      ]);
      const packetData = createPacketData(RouteType.FLOOD, PayloadType.PATH, payloadData);

      const packet = new Packet(packetData);
      const decoded = packet.decode() as PathPayload;

      expect(decoded.payloadType).toBe(PayloadType.PATH);
      expect(decoded.dstHash).toBe('88');
      expect(decoded.srcHash).toBe('99');
    });
  });

  describe('decode - TRACE payload', () => {
    it('should decode TRACE payload', () => {
      const payloadData = new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05]);
      const packetData = createPacketData(RouteType.FLOOD, PayloadType.TRACE, payloadData);

      const packet = new Packet(packetData);
      const decoded = packet.decode() as TracePayload;

      expect(decoded.payloadType).toBe(PayloadType.TRACE);
      expect(decoded.data).toEqual(payloadData);
    });
  });

  describe('decode - MULTIPART payload', () => {
    it('should decode MULTIPART payload', () => {
      const payloadData = new Uint8Array([0xAA, 0xBB, 0xCC, 0xDD]);
      const packetData = createPacketData(RouteType.FLOOD, PayloadType.MULTIPART, payloadData);

      const packet = new Packet(packetData);
      const decoded = packet.decode() as MultipartPayload;

      expect(decoded.payloadType).toBe(PayloadType.MULTIPART);
      expect(decoded.data).toEqual(payloadData);
    });
  });

  describe('decode - CONTROL payload', () => {
    it('should decode CONTROL payload', () => {
      const flags = 0x05;
      const data = new Uint8Array([0x11, 0x22, 0x33]);
      const payloadData = new Uint8Array([flags, ...data]);
      const packetData = createPacketData(RouteType.FLOOD, PayloadType.CONTROL, payloadData);

      const packet = new Packet(packetData);
      const decoded = packet.decode() as ControlPayload;

      expect(decoded.payloadType).toBe(PayloadType.CONTROL);
      expect(decoded.flags).toBe(flags);
      expect(decoded.data).toEqual(data);
    });
  });

  describe('decode - RAW_CUSTOM payload', () => {
    it('should decode RAW_CUSTOM payload', () => {
      const payloadData = new Uint8Array([0xFF, 0xEE, 0xDD, 0xCC, 0xBB]);
      const packetData = createPacketData(RouteType.FLOOD, PayloadType.RAW_CUSTOM, payloadData);

      const packet = new Packet(packetData);
      const decoded = packet.decode() as RawCustomPayload;

      expect(decoded.payloadType).toBe(PayloadType.RAW_CUSTOM);
      expect(decoded.data).toEqual(payloadData);
    });
  });

  describe('decode - all payload types', () => {
    it('should successfully decode all defined payload types', () => {
      const payloadTypes = [
        PayloadType.REQUEST,
        PayloadType.RESPONSE,
        PayloadType.TEXT,
        PayloadType.ACK,
        PayloadType.ADVERT,
        PayloadType.GROUP_TEXT,
        PayloadType.GROUP_DATA,
        PayloadType.ANON_REQ,
        PayloadType.PATH,
        PayloadType.TRACE,
        PayloadType.MULTIPART,
        PayloadType.CONTROL,
        PayloadType.RAW_CUSTOM
      ];

      payloadTypes.forEach(type => {
        let payloadData: Uint8Array;

        // Create appropriate payload data for each type
        switch (type) {
          case PayloadType.ADVERT: {
            const publicKey = new Uint8Array(32).fill(0x42);
            const signature = new Uint8Array(64).fill(0xAA);
            const timestamp = 1234567890;
            payloadData = new Uint8Array([
              ...publicKey,
              timestamp & 0xFF, (timestamp >> 8) & 0xFF, (timestamp >> 16) & 0xFF, (timestamp >> 24) & 0xFF,
              ...signature,
              0x00 // flags
            ]);
            break;
          }
          case PayloadType.ANON_REQ: {
            const publicKey = new Uint8Array(32).fill(0x99);
            payloadData = new Uint8Array([
              0x77, // dstHash
              ...publicKey,
              0x12, 0x34, // cipherMAC
              0xAA // cipherText
            ]);
            break;
          }
          case PayloadType.GROUP_TEXT:
          case PayloadType.GROUP_DATA:
            payloadData = new Uint8Array([
              0x9C, // channelHash
              0xAA, 0xBB, // cipherMAC
              0x01 // cipherText
            ]);
            break;
          case PayloadType.REQUEST:
          case PayloadType.RESPONSE:
          case PayloadType.TEXT:
          case PayloadType.ACK:
          case PayloadType.PATH:
            payloadData = new Uint8Array([
              0xAB, // dstHash
              0xCD, // srcHash
              0x12, 0x34, // cipherMAC
              0x01 // cipherText
            ]);
            break;
          case PayloadType.CONTROL:
            payloadData = new Uint8Array([0x05, 0x01, 0x02]);
            break;
          default:
            payloadData = new Uint8Array([0x01, 0x02, 0x03]);
        }

        const packetData = createPacketData(RouteType.FLOOD, type, payloadData);
        const packet = new Packet(packetData);

        expect(() => packet.decode()).not.toThrow();
        const decoded = packet.decode();
        expect(decoded.payloadType).toBe(type);
      });
    });

    it('should throw error for unknown payload type', () => {
      const unknownType = 0x0E; // Not defined in PayloadType
      const payloadData = new Uint8Array([0x01, 0x02]);
      const packetData = createPacketData(RouteType.FLOOD, unknownType, payloadData);

      const packet = new Packet(packetData);
      expect(() => packet.decode()).toThrow(`can't decode payload ${unknownType}`);
    });
  });

  describe('hash', () => {
    it('should compute hash for non-TRACE packet', () => {
      const payloadData = new Uint8Array([0x01, 0x02, 0x03]);
      const packetData = createPacketData(RouteType.FLOOD, PayloadType.RAW_CUSTOM, payloadData);

      const packet = new Packet(packetData);
      const hash = packet.hash();

      expect(hash).toBeInstanceOf(Uint8Array);
      expect(hash.length).toBe(8);
    });

    it('should compute hash for TRACE packet including path', () => {
      const payloadData = new Uint8Array([0x01, 0x02, 0x03]);
      const packetData = createPacketData(RouteType.FLOOD, PayloadType.TRACE, payloadData);

      const packet = new Packet(packetData);
      const hash = packet.hash();

      expect(hash).toBeInstanceOf(Uint8Array);
      expect(hash.length).toBe(8);
    });
  });

  describe('path handling', () => {
    it('should parse packet with path', () => {
      const header = (RouteType.FLOOD & 0x03) | ((PayloadType.TRACE & 0x0F) << 2) | (0x01 << 6);
      const pathLength = 0x02; // 2 hashes, 1 byte each (hashSize=1, hashCount=2)
      const path = new Uint8Array([0xAA, 0xBB]);
      const payloadData = new Uint8Array([0x01, 0x02]);
      const packetData = new Uint8Array([header, pathLength, ...path, ...payloadData]);

      const packet = new Packet(packetData);

      expect(packet.getPathHashCount()).toBe(2);
      expect(packet.getPathHashSize()).toBe(1);
      expect(packet.getPathBytesLength()).toBe(2);
    });
  });
});
