import 'dart:convert';
import 'dart:typed_data';

import 'package:flutter_test/flutter_test.dart';
import 'package:tane/data/export_import/inventory_json_codec.dart';
import 'package:tane/data/export_import/inventory_snapshot.dart';
import 'package:tane/db/database.dart';
import 'package:tane/db/enums.dart';

void main() {
  const codec = InventoryJsonCodec();

  const variety = Variety(
    id: 'var-1',
    createdAt: 1000,
    updatedAt: '000000000001000:00000:node-a',
    lastAuthor: 'node-a',
    isDeleted: false,
    schemaRowVersion: 1,
    label: 'Grandma tomato',
    speciesId: 'sp-1',
    cultivarName: 'Marmande',
    category: 'Solanaceae',
    notes: 'Keeps well',
    isDraft: false,
    isOrganic: false,
    needsReproduction: false,
  );
  const lot = Lot(
    id: 'lot-1',
    createdAt: 1001,
    updatedAt: '000000000001001:00000:node-a',
    lastAuthor: 'node-a',
    isDeleted: false,
    schemaRowVersion: 1,
    varietyId: 'var-1',
    type: LotType.seed,
    harvestYear: 2024,
    harvestMonth: 6,
    quantityKind: 'cob',
    quantityPrecise: 2,
    quantityLabel: null,
    presentation: null,
    storageLocation: 'fridge',
    offerStatus: OfferStatus.private,
    seedbankId: null,
  );
  const name = VarietyVernacularName(
    id: 'name-1',
    createdAt: 1002,
    updatedAt: '000000000001002:00000:node-a',
    lastAuthor: 'node-a',
    isDeleted: false,
    schemaRowVersion: 1,
    varietyId: 'var-1',
    name: 'tomate de la abuela',
    language: 'es',
    region: 'Madrid',
  );
  const link = ExternalLink(
    id: 'link-1',
    createdAt: 1003,
    updatedAt: '000000000001003:00000:node-a',
    lastAuthor: 'node-a',
    isDeleted: false,
    schemaRowVersion: 1,
    parentType: ParentType.variety,
    parentId: 'var-1',
    url: 'https://example.org/tomato',
    title: 'Wiki',
  );
  const germination = GerminationTest(
    id: 'germ-1',
    createdAt: 1004,
    updatedAt: '000000000001004:00000:node-a',
    lastAuthor: 'node-a',
    isDeleted: false,
    schemaRowVersion: 1,
    lotId: 'lot-1',
    testedOn: 1700000000000,
    sampleSize: 10,
    germinatedCount: 8,
    notes: null,
  );
  const movement = Movement(
    id: 'mov-1',
    createdAt: 1005,
    lastAuthor: 'node-a',
    schemaRowVersion: 1,
    lotId: 'lot-1',
    type: MovementType.received,
    occurredOn: 1690000000000,
    counterpartyId: 'party-1',
    quantityKind: 'handful',
    quantityPrecise: null,
    quantityLabel: 'a handful',
    parentMovementId: null,
    plantareId: null,
    notes: 'from the fair',
  );
  const party = Party(
    id: 'party-1',
    createdAt: 1006,
    updatedAt: '000000000001006:00000:node-a',
    lastAuthor: 'node-a',
    isDeleted: false,
    schemaRowVersion: 1,
    displayName: 'Village bank',
    publicKey: null,
    kind: PartyKind.collective,
    note: null,
  );
  final photo = Attachment(
    id: 'att-1',
    createdAt: 1007,
    updatedAt: '000000000001007:00000:node-a',
    lastAuthor: 'node-a',
    isDeleted: false,
    schemaRowVersion: 1,
    parentType: ParentType.variety,
    parentId: 'var-1',
    kind: AttachmentKind.photo,
    uri: null,
    bytes: Uint8List.fromList([1, 2, 3, 255]),
    mimeType: 'image/jpeg',
    sortOrder: -1,
  );

  final snapshot = InventorySnapshot(
    varieties: const [variety],
    lots: const [lot],
    vernacularNames: const [name],
    externalLinks: const [link],
    germinationTests: const [germination],
    movements: const [movement],
    parties: const [party],
    attachments: [photo],
    speciesNamesById: const {'sp-1': 'Solanum lycopersicum'},
  );

  test('encode → decode round-trips every table and field', () {
    final decoded = codec.decode(codec.encode(snapshot));

    expect(decoded.varieties, [variety]);
    expect(decoded.lots, [lot]);
    expect(decoded.vernacularNames, [name]);
    expect(decoded.externalLinks, [link]);
    expect(decoded.germinationTests, [germination]);
    expect(decoded.movements, [movement]);
    expect(decoded.parties, [party]);
    expect(decoded.attachments, [photo]);
    expect(decoded.speciesNamesById, {'sp-1': 'Solanum lycopersicum'});
  });

  test('photo bytes travel as base64', () {
    final root = jsonDecode(codec.encode(snapshot)) as Map<String, dynamic>;
    final attachment =
        (root['attachments'] as List).single as Map<String, dynamic>;
    expect(attachment['bytesBase64'], base64Encode([1, 2, 3, 255]));
    expect(attachment.containsKey('bytes'), isFalse);
  });

  test('envelope carries format/schema versions and app id', () {
    final root = jsonDecode(codec.encode(snapshot)) as Map<String, dynamic>;
    expect(root['formatVersion'], inventoryFormatVersion);
    expect(root['schemaVersion'], AppDatabase.currentSchemaVersion);
    expect(root['appId'], inventoryAppId);
  });

  test('unknown fields are ignored (forward compatibility, §5.2)', () {
    final root = jsonDecode(codec.encode(snapshot)) as Map<String, dynamic>;
    root['someFutureSection'] = {'x': 1};
    ((root['varieties'] as List).single
            as Map<String, dynamic>)['futureField'] =
        'ignored';
    final decoded = codec.decode(jsonEncode(root));
    expect(decoded.varieties, [variety]);
  });

  test('unknown enum values fall back or drop the row (§5.2)', () {
    final root = jsonDecode(codec.encode(snapshot)) as Map<String, dynamic>;
    ((root['lots'] as List).single as Map<String, dynamic>)['type'] =
        'hologram';
    ((root['movements'] as List).single as Map<String, dynamic>)['type'] =
        'teleported';
    final decoded = codec.decode(jsonEncode(root));
    expect(decoded.lots.single.type, LotType.seed); // safe default
    expect(decoded.movements, isEmpty); // no safe default → dropped
  });

  test('a broken row is skipped without aborting the import', () {
    final root = jsonDecode(codec.encode(snapshot)) as Map<String, dynamic>;
    (root['varieties'] as List).add({'id': 'var-2'}); // missing fields
    final decoded = codec.decode(jsonEncode(root));
    expect(decoded.varieties, [variety]);
  });

  test('a newer formatVersion is rejected with a clear error', () {
    final root = jsonDecode(codec.encode(snapshot)) as Map<String, dynamic>;
    root['formatVersion'] = inventoryFormatVersion + 1;
    expect(() => codec.decode(jsonEncode(root)), throwsFormatException);
  });

  test('malformed input is rejected', () {
    expect(() => codec.decode('not json'), throwsFormatException);
    expect(() => codec.decode('[1,2]'), throwsFormatException);
    expect(() => codec.decode('{"foo": 1}'), throwsFormatException);
  });
}
